1846 lines
55 KiB
C++
1846 lines
55 KiB
C++
/*
|
|
* Copyright (c) 2003-2012, John Wiegley. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are
|
|
* met:
|
|
*
|
|
* - Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
*
|
|
* - Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* - Neither the name of New Artisans LLC nor the names of its
|
|
* contributors may be used to endorse or promote products derived from
|
|
* this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#include <system.hh>
|
|
|
|
#include "times.h"
|
|
|
|
namespace ledger {
|
|
|
|
optional<datetime_t> epoch;
|
|
|
|
date_time::weekdays start_of_week = gregorian::Sunday;
|
|
|
|
//#define USE_BOOST_FACETS 1
|
|
#if defined(USE_BOOST_FACETS)
|
|
#error "Boost facets are not quite working yet"
|
|
#endif
|
|
|
|
namespace {
|
|
template <typename T, typename InputFacetType, typename OutputFacetType>
|
|
class temporal_io_t : public noncopyable
|
|
{
|
|
const char * fmt_str;
|
|
#if defined(USE_BOOST_FACETS)
|
|
std::istringstream input_stream;
|
|
std::ostringstream output_stream;
|
|
InputFacetType * input_facet;
|
|
OutputFacetType * output_facet;
|
|
std::string temp_string;
|
|
#endif // USE_BOOST_FACETS
|
|
|
|
public:
|
|
date_traits_t traits;
|
|
bool input;
|
|
|
|
temporal_io_t(const char * _fmt_str, bool _input)
|
|
: fmt_str(_fmt_str),
|
|
traits(icontains(fmt_str, "%y"),
|
|
icontains(fmt_str, "%m") || icontains(fmt_str, "%b"),
|
|
icontains(fmt_str, "%d")),
|
|
input(_input) {
|
|
#if defined(USE_BOOST_FACETS)
|
|
if (input) {
|
|
input_facet = new InputFacetType(fmt_str);
|
|
input_stream.imbue(std::locale(std::locale::classic(), input_facet));
|
|
} else {
|
|
output_facet = new OutputFacetType(fmt_str);
|
|
output_stream.imbue(std::locale(std::locale::classic(), output_facet));
|
|
}
|
|
#endif // USE_BOOST_FACETS
|
|
}
|
|
|
|
void set_format(const char * fmt) {
|
|
fmt_str = fmt;
|
|
traits = date_traits_t(icontains(fmt_str, "%y"),
|
|
icontains(fmt_str, "%m") ||
|
|
icontains(fmt_str, "%b"),
|
|
icontains(fmt_str, "%d"));
|
|
#if defined(USE_BOOST_FACETS)
|
|
if (input)
|
|
input_facet->format(fmt_str);
|
|
else
|
|
output_facet->format(fmt_str);
|
|
#endif // USE_BOOST_FACETS
|
|
}
|
|
|
|
T parse(const char *) {}
|
|
|
|
std::string format(const T& when) {
|
|
#if defined(USE_BOOST_FACETS)
|
|
output_stream.str(temp_string);
|
|
output_stream.seekp(std::ios_base::beg);
|
|
output_stream.clear();
|
|
output_stream << when;
|
|
return output_stream.str();
|
|
#else // USE_BOOST_FACETS
|
|
std::tm data(to_tm(when));
|
|
char buf[128];
|
|
std::strftime(buf, 127, fmt_str, &data);
|
|
return buf;
|
|
#endif // USE_BOOST_FACETS
|
|
}
|
|
};
|
|
|
|
template <>
|
|
datetime_t temporal_io_t<datetime_t, posix_time::time_input_facet,
|
|
posix_time::time_facet>
|
|
::parse(const char * str)
|
|
{
|
|
#if defined(USE_BOOST_FACETS)
|
|
input_stream.seekg(std::ios_base::beg);
|
|
input_stream.clear();
|
|
input_stream.str(str);
|
|
|
|
datetime_t when;
|
|
input_stream >> when;
|
|
#if defined(DEBUG_ON)
|
|
if (when.is_not_a_date_time())
|
|
DEBUG("times.parse", "Failed to parse date/time '" << str
|
|
<< "' using pattern '" << fmt_str << "'");
|
|
#endif
|
|
|
|
if (! when.is_not_a_date_time() &&
|
|
input_stream.good() && ! input_stream.eof() &&
|
|
input_stream.peek() != EOF) {
|
|
DEBUG("times.parse", "This string has leftovers: '" << str << "'");
|
|
return datetime_t();
|
|
}
|
|
return when;
|
|
#else // USE_BOOST_FACETS
|
|
std::tm data;
|
|
std::memset(&data, 0, sizeof(std::tm));
|
|
if (strptime(str, fmt_str, &data))
|
|
return posix_time::ptime_from_tm(data);
|
|
else
|
|
return datetime_t();
|
|
#endif // USE_BOOST_FACETS
|
|
}
|
|
|
|
template <>
|
|
date_t temporal_io_t<date_t, gregorian::date_input_facet,
|
|
gregorian::date_facet>
|
|
::parse(const char * str)
|
|
{
|
|
#if defined(USE_BOOST_FACETS)
|
|
input_stream.seekg(std::ios_base::beg);
|
|
input_stream.clear();
|
|
input_stream.str(str);
|
|
|
|
date_t when;
|
|
input_stream >> when;
|
|
#if defined(DEBUG_ON)
|
|
if (when.is_not_a_date())
|
|
DEBUG("times.parse", "Failed to parse date '" << str
|
|
<< "' using pattern '" << fmt_str << "'");
|
|
#endif
|
|
|
|
if (! when.is_not_a_date() &&
|
|
input_stream.good() && ! input_stream.eof() &&
|
|
input_stream.peek() != EOF) {
|
|
DEBUG("times.parse", "This string has leftovers: '" << str << "'");
|
|
return date_t();
|
|
}
|
|
return when;
|
|
#else // USE_BOOST_FACETS
|
|
std::tm data;
|
|
std::memset(&data, 0, sizeof(std::tm));
|
|
data.tm_year = CURRENT_DATE().year() - 1900;
|
|
data.tm_mday = 1; // some formats have no day
|
|
if (strptime(str, fmt_str, &data))
|
|
return gregorian::date_from_tm(data);
|
|
else
|
|
return date_t();
|
|
#endif // USE_BOOST_FACETS
|
|
}
|
|
|
|
typedef temporal_io_t<datetime_t, posix_time::time_input_facet,
|
|
posix_time::time_facet> datetime_io_t;
|
|
typedef temporal_io_t<date_t, gregorian::date_input_facet,
|
|
gregorian::date_facet> date_io_t;
|
|
|
|
shared_ptr<datetime_io_t> input_datetime_io;
|
|
shared_ptr<datetime_io_t> timelog_datetime_io;
|
|
shared_ptr<date_io_t> input_date_io;
|
|
shared_ptr<datetime_io_t> written_datetime_io;
|
|
shared_ptr<date_io_t> written_date_io;
|
|
shared_ptr<datetime_io_t> printed_datetime_io;
|
|
shared_ptr<date_io_t> printed_date_io;
|
|
|
|
std::deque<shared_ptr<date_io_t> > readers;
|
|
|
|
bool convert_separators_to_slashes = true;
|
|
|
|
date_t parse_date_mask_routine(const char * date_str, date_io_t& io,
|
|
date_traits_t * traits = NULL)
|
|
{
|
|
VERIFY(std::strlen(date_str) < 127);
|
|
|
|
char buf[128];
|
|
std::strcpy(buf, date_str);
|
|
|
|
if (convert_separators_to_slashes) {
|
|
for (char * p = buf; *p; p++)
|
|
if (*p == '.' || *p == '-')
|
|
*p = '/';
|
|
}
|
|
|
|
date_t when = io.parse(buf);
|
|
|
|
if (! when.is_not_a_date()) {
|
|
DEBUG("times.parse", "Passed date string: " << date_str);
|
|
DEBUG("times.parse", "Parsed date string: " << buf);
|
|
DEBUG("times.parse", "Parsed result is: " << when);
|
|
DEBUG("times.parse", "Formatted result is: " << io.format(when));
|
|
|
|
string when_str = io.format(when);
|
|
|
|
const char * p = when_str.c_str();
|
|
const char * q = buf;
|
|
for (; *p && *q; p++, q++) {
|
|
if (*p != *q && *p == '0') p++;
|
|
if (! *p || *p != *q) break;
|
|
}
|
|
if (*p != '\0' || *q != '\0')
|
|
throw_(date_error, _("Invalid date: %1") << date_str);
|
|
|
|
if (traits)
|
|
*traits = io.traits;
|
|
|
|
if (! io.traits.has_year) {
|
|
when = date_t(CURRENT_DATE().year(), when.month(), when.day());
|
|
|
|
if (when.month() > CURRENT_DATE().month())
|
|
when -= gregorian::years(1);
|
|
}
|
|
}
|
|
return when;
|
|
}
|
|
|
|
date_t parse_date_mask(const char * date_str, date_traits_t * traits = NULL)
|
|
{
|
|
if (input_date_io.get()) {
|
|
date_t when = parse_date_mask_routine(date_str, *input_date_io.get(),
|
|
traits);
|
|
if (! when.is_not_a_date())
|
|
return when;
|
|
}
|
|
|
|
foreach (shared_ptr<date_io_t>& reader, readers) {
|
|
date_t when = parse_date_mask_routine(date_str, *reader.get(), traits);
|
|
if (! when.is_not_a_date())
|
|
return when;
|
|
}
|
|
|
|
throw_(date_error, _("Invalid date: %1") << date_str);
|
|
return date_t();
|
|
}
|
|
}
|
|
|
|
optional<date_time::weekdays> string_to_day_of_week(const std::string& str)
|
|
{
|
|
if (str == _("sun") || str == _("sunday") || str == "0")
|
|
return gregorian::Sunday;
|
|
else if (str == _("mon") || str == _("monday") || str == "1")
|
|
return gregorian::Monday;
|
|
else if (str == _("tue") || str == _("tuesday") || str == "2")
|
|
return gregorian::Tuesday;
|
|
else if (str == _("wed") || str == _("wednesday") || str == "3")
|
|
return gregorian::Wednesday;
|
|
else if (str == _("thu") || str == _("thursday") || str == "4")
|
|
return gregorian::Thursday;
|
|
else if (str == _("fri") || str == _("friday") || str == "5")
|
|
return gregorian::Friday;
|
|
else if (str == _("sat") || str == _("saturday") || str == "6")
|
|
return gregorian::Saturday;
|
|
else
|
|
return none;
|
|
}
|
|
|
|
optional<date_time::months_of_year>
|
|
string_to_month_of_year(const std::string& str)
|
|
{
|
|
if (str == _("jan") || str == _("january") || str == "0")
|
|
return gregorian::Jan;
|
|
else if (str == _("feb") || str == _("february") || str == "1")
|
|
return gregorian::Feb;
|
|
else if (str == _("mar") || str == _("march") || str == "2")
|
|
return gregorian::Mar;
|
|
else if (str == _("apr") || str == _("april") || str == "3")
|
|
return gregorian::Apr;
|
|
else if (str == _("may") || str == _("may") || str == "4")
|
|
return gregorian::May;
|
|
else if (str == _("jun") || str == _("june") || str == "5")
|
|
return gregorian::Jun;
|
|
else if (str == _("jul") || str == _("july") || str == "6")
|
|
return gregorian::Jul;
|
|
else if (str == _("aug") || str == _("august") || str == "7")
|
|
return gregorian::Aug;
|
|
else if (str == _("sep") || str == _("september") || str == "8")
|
|
return gregorian::Sep;
|
|
else if (str == _("oct") || str == _("october") || str == "9")
|
|
return gregorian::Oct;
|
|
else if (str == _("nov") || str == _("november") || str == "10")
|
|
return gregorian::Nov;
|
|
else if (str == _("dec") || str == _("december") || str == "11")
|
|
return gregorian::Dec;
|
|
else
|
|
return none;
|
|
}
|
|
|
|
datetime_t parse_datetime(const char * str)
|
|
{
|
|
char buf[128];
|
|
std::strcpy(buf, str);
|
|
|
|
for (char * p = buf; *p; p++)
|
|
if (*p == '.' || *p == '-')
|
|
*p = '/';
|
|
|
|
datetime_t when = input_datetime_io->parse(buf);
|
|
if (when.is_not_a_date_time()) {
|
|
when = timelog_datetime_io->parse(buf);
|
|
if (when.is_not_a_date_time()) {
|
|
throw_(date_error, _("Invalid date/time: %1") << str);
|
|
}
|
|
}
|
|
return when;
|
|
}
|
|
|
|
date_t parse_date(const char * str)
|
|
{
|
|
return parse_date_mask(str);
|
|
}
|
|
|
|
date_t date_specifier_t::begin() const
|
|
{
|
|
year_type the_year = year ? *year : year_type(CURRENT_DATE().year());
|
|
month_type the_month = month ? *month : date_t::month_type(1);
|
|
day_type the_day = day ? *day : date_t::day_type(1);
|
|
|
|
#if !defined(NO_ASSERTS)
|
|
if (day)
|
|
assert(! wday);
|
|
else if (wday)
|
|
assert(! day);
|
|
#endif
|
|
|
|
// jww (2009-11-16): Handle wday. If a month is set, find the most recent
|
|
// wday in that month; if the year is set, then in that year.
|
|
|
|
return gregorian::date(static_cast<date_t::year_type>(the_year),
|
|
static_cast<date_t::month_type>(the_month),
|
|
static_cast<date_t::day_type>(the_day));
|
|
}
|
|
|
|
date_t date_specifier_t::end() const
|
|
{
|
|
if (day || wday)
|
|
return begin() + gregorian::days(1);
|
|
else if (month)
|
|
return begin() + gregorian::months(1);
|
|
else if (year)
|
|
return begin() + gregorian::years(1);
|
|
else {
|
|
assert(false);
|
|
return date_t();
|
|
}
|
|
}
|
|
|
|
std::ostream& operator<<(std::ostream& out,
|
|
const date_duration_t& duration)
|
|
{
|
|
if (duration.quantum == date_duration_t::DAYS)
|
|
out << duration.length << " day(s)";
|
|
else if (duration.quantum == date_duration_t::WEEKS)
|
|
out << duration.length << " week(s)";
|
|
else if (duration.quantum == date_duration_t::MONTHS)
|
|
out << duration.length << " month(s)";
|
|
else if (duration.quantum == date_duration_t::QUARTERS)
|
|
out << duration.length << " quarter(s)";
|
|
else {
|
|
assert(duration.quantum == date_duration_t::YEARS);
|
|
out << duration.length << " year(s)";
|
|
}
|
|
return out;
|
|
}
|
|
|
|
class date_parser_t
|
|
{
|
|
friend void show_period_tokens(std::ostream& out, const string& arg);
|
|
|
|
class lexer_t
|
|
{
|
|
friend class date_parser_t;
|
|
|
|
string::const_iterator begin;
|
|
string::const_iterator end;
|
|
|
|
public:
|
|
struct token_t
|
|
{
|
|
enum kind_t {
|
|
UNKNOWN,
|
|
|
|
TOK_DATE,
|
|
TOK_INT,
|
|
TOK_SLASH,
|
|
TOK_DASH,
|
|
TOK_DOT,
|
|
|
|
TOK_A_YEAR,
|
|
TOK_A_MONTH,
|
|
TOK_A_WDAY,
|
|
|
|
TOK_AGO,
|
|
TOK_HENCE,
|
|
TOK_SINCE,
|
|
TOK_UNTIL,
|
|
TOK_IN,
|
|
TOK_THIS,
|
|
TOK_NEXT,
|
|
TOK_LAST,
|
|
TOK_EVERY,
|
|
|
|
TOK_TODAY,
|
|
TOK_TOMORROW,
|
|
TOK_YESTERDAY,
|
|
|
|
TOK_YEAR,
|
|
TOK_QUARTER,
|
|
TOK_MONTH,
|
|
TOK_WEEK,
|
|
TOK_DAY,
|
|
|
|
TOK_YEARLY,
|
|
TOK_QUARTERLY,
|
|
TOK_BIMONTHLY,
|
|
TOK_MONTHLY,
|
|
TOK_BIWEEKLY,
|
|
TOK_WEEKLY,
|
|
TOK_DAILY,
|
|
|
|
TOK_YEARS,
|
|
TOK_QUARTERS,
|
|
TOK_MONTHS,
|
|
TOK_WEEKS,
|
|
TOK_DAYS,
|
|
|
|
END_REACHED
|
|
|
|
} kind;
|
|
|
|
typedef variant<unsigned short,
|
|
string,
|
|
date_specifier_t::year_type,
|
|
date_time::months_of_year,
|
|
date_time::weekdays,
|
|
date_specifier_t> content_t;
|
|
|
|
optional<content_t> value;
|
|
|
|
explicit token_t(kind_t _kind = UNKNOWN,
|
|
const optional<content_t>& _value =
|
|
content_t(empty_string))
|
|
: kind(_kind), value(_value) {
|
|
TRACE_CTOR(date_parser_t::lexer_t::token_t, "");
|
|
}
|
|
token_t(const token_t& tok)
|
|
: kind(tok.kind), value(tok.value) {
|
|
TRACE_CTOR(date_parser_t::lexer_t::token_t, "copy");
|
|
}
|
|
~token_t() throw() {
|
|
TRACE_DTOR(date_parser_t::lexer_t::token_t);
|
|
}
|
|
|
|
token_t& operator=(const token_t& tok) {
|
|
if (this != &tok) {
|
|
kind = tok.kind;
|
|
value = tok.value;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
operator bool() const {
|
|
return kind != END_REACHED;
|
|
}
|
|
|
|
string to_string() const {
|
|
std::ostringstream out;
|
|
|
|
switch (kind) {
|
|
case UNKNOWN:
|
|
out << boost::get<string>(*value);
|
|
break;
|
|
case TOK_DATE:
|
|
return boost::get<date_specifier_t>(*value).to_string();
|
|
case TOK_INT:
|
|
out << boost::get<unsigned short>(*value);
|
|
break;
|
|
case TOK_SLASH: return "/";
|
|
case TOK_DASH: return "-";
|
|
case TOK_DOT: return ".";
|
|
case TOK_A_YEAR:
|
|
out << boost::get<date_specifier_t::year_type>(*value);
|
|
break;
|
|
case TOK_A_MONTH:
|
|
out << date_specifier_t::month_type
|
|
(boost::get<date_time::months_of_year>(*value));
|
|
break;
|
|
case TOK_A_WDAY:
|
|
out << date_specifier_t::day_of_week_type
|
|
(boost::get<date_time::weekdays>(*value));
|
|
break;
|
|
case TOK_AGO: return "ago";
|
|
case TOK_HENCE: return "hence";
|
|
case TOK_SINCE: return "since";
|
|
case TOK_UNTIL: return "until";
|
|
case TOK_IN: return "in";
|
|
case TOK_THIS: return "this";
|
|
case TOK_NEXT: return "next";
|
|
case TOK_LAST: return "last";
|
|
case TOK_EVERY: return "every";
|
|
case TOK_TODAY: return "today";
|
|
case TOK_TOMORROW: return "tomorrow";
|
|
case TOK_YESTERDAY: return "yesterday";
|
|
case TOK_YEAR: return "year";
|
|
case TOK_QUARTER: return "quarter";
|
|
case TOK_MONTH: return "month";
|
|
case TOK_WEEK: return "week";
|
|
case TOK_DAY: return "day";
|
|
case TOK_YEARLY: return "yearly";
|
|
case TOK_QUARTERLY: return "quarterly";
|
|
case TOK_BIMONTHLY: return "bimonthly";
|
|
case TOK_MONTHLY: return "monthly";
|
|
case TOK_BIWEEKLY: return "biweekly";
|
|
case TOK_WEEKLY: return "weekly";
|
|
case TOK_DAILY: return "daily";
|
|
case TOK_YEARS: return "years";
|
|
case TOK_QUARTERS: return "quarters";
|
|
case TOK_MONTHS: return "months";
|
|
case TOK_WEEKS: return "weeks";
|
|
case TOK_DAYS: return "days";
|
|
case END_REACHED: return "<EOF>";
|
|
}
|
|
|
|
return out.str();
|
|
}
|
|
|
|
void dump(std::ostream& out) const {
|
|
switch (kind) {
|
|
case UNKNOWN: out << "UNKNOWN"; break;
|
|
case TOK_DATE: out << "TOK_DATE"; break;
|
|
case TOK_INT: out << "TOK_INT"; break;
|
|
case TOK_SLASH: out << "TOK_SLASH"; break;
|
|
case TOK_DASH: out << "TOK_DASH"; break;
|
|
case TOK_DOT: out << "TOK_DOT"; break;
|
|
case TOK_A_YEAR: out << "TOK_A_YEAR"; break;
|
|
case TOK_A_MONTH: out << "TOK_A_MONTH"; break;
|
|
case TOK_A_WDAY: out << "TOK_A_WDAY"; break;
|
|
case TOK_AGO: out << "TOK_AGO"; break;
|
|
case TOK_HENCE: out << "TOK_HENCE"; break;
|
|
case TOK_SINCE: out << "TOK_SINCE"; break;
|
|
case TOK_UNTIL: out << "TOK_UNTIL"; break;
|
|
case TOK_IN: out << "TOK_IN"; break;
|
|
case TOK_THIS: out << "TOK_THIS"; break;
|
|
case TOK_NEXT: out << "TOK_NEXT"; break;
|
|
case TOK_LAST: out << "TOK_LAST"; break;
|
|
case TOK_EVERY: out << "TOK_EVERY"; break;
|
|
case TOK_TODAY: out << "TOK_TODAY"; break;
|
|
case TOK_TOMORROW: out << "TOK_TOMORROW"; break;
|
|
case TOK_YESTERDAY: out << "TOK_YESTERDAY"; break;
|
|
case TOK_YEAR: out << "TOK_YEAR"; break;
|
|
case TOK_QUARTER: out << "TOK_QUARTER"; break;
|
|
case TOK_MONTH: out << "TOK_MONTH"; break;
|
|
case TOK_WEEK: out << "TOK_WEEK"; break;
|
|
case TOK_DAY: out << "TOK_DAY"; break;
|
|
case TOK_YEARLY: out << "TOK_YEARLY"; break;
|
|
case TOK_QUARTERLY: out << "TOK_QUARTERLY"; break;
|
|
case TOK_BIMONTHLY: out << "TOK_BIMONTHLY"; break;
|
|
case TOK_MONTHLY: out << "TOK_MONTHLY"; break;
|
|
case TOK_BIWEEKLY: out << "TOK_BIWEEKLY"; break;
|
|
case TOK_WEEKLY: out << "TOK_WEEKLY"; break;
|
|
case TOK_DAILY: out << "TOK_DAILY"; break;
|
|
case TOK_YEARS: out << "TOK_YEARS"; break;
|
|
case TOK_QUARTERS: out << "TOK_QUARTERS"; break;
|
|
case TOK_MONTHS: out << "TOK_MONTHS"; break;
|
|
case TOK_WEEKS: out << "TOK_WEEKS"; break;
|
|
case TOK_DAYS: out << "TOK_DAYS"; break;
|
|
case END_REACHED: out << "END_REACHED"; break;
|
|
}
|
|
}
|
|
|
|
void unexpected();
|
|
static void expected(char wanted, char c = '\0');
|
|
};
|
|
|
|
token_t token_cache;
|
|
|
|
lexer_t(string::const_iterator _begin,
|
|
string::const_iterator _end)
|
|
: begin(_begin), end(_end)
|
|
{
|
|
TRACE_CTOR(date_parser_t::lexer_t, "");
|
|
}
|
|
lexer_t(const lexer_t& other)
|
|
: begin(other.begin), end(other.end),
|
|
token_cache(other.token_cache)
|
|
{
|
|
TRACE_CTOR(date_parser_t::lexer_t, "copy");
|
|
}
|
|
~lexer_t() throw() {
|
|
TRACE_DTOR(date_parser_t::lexer_t);
|
|
}
|
|
|
|
token_t next_token();
|
|
void push_token(token_t tok) {
|
|
assert(token_cache.kind == token_t::UNKNOWN);
|
|
token_cache = tok;
|
|
}
|
|
token_t peek_token() {
|
|
if (token_cache.kind == token_t::UNKNOWN)
|
|
token_cache = next_token();
|
|
return token_cache;
|
|
}
|
|
};
|
|
|
|
string arg;
|
|
lexer_t lexer;
|
|
|
|
public:
|
|
date_parser_t(const string& _arg)
|
|
: arg(_arg), lexer(arg.begin(), arg.end()) {
|
|
TRACE_CTOR(date_parser_t, "");
|
|
}
|
|
date_parser_t(const date_parser_t& parser)
|
|
: arg(parser.arg), lexer(parser.lexer) {
|
|
TRACE_CTOR(date_parser_t, "copy");
|
|
}
|
|
~date_parser_t() throw() {
|
|
TRACE_DTOR(date_parser_t);
|
|
}
|
|
|
|
date_interval_t parse();
|
|
|
|
private:
|
|
void determine_when(lexer_t::token_t& tok, date_specifier_t& specifier);
|
|
};
|
|
|
|
void date_parser_t::determine_when(date_parser_t::lexer_t::token_t& tok,
|
|
date_specifier_t& specifier)
|
|
{
|
|
date_t today = CURRENT_DATE();
|
|
|
|
switch (tok.kind) {
|
|
case lexer_t::token_t::TOK_DATE:
|
|
specifier = boost::get<date_specifier_t>(*tok.value);
|
|
break;
|
|
|
|
case lexer_t::token_t::TOK_INT: {
|
|
unsigned short amount = boost::get<unsigned short>(*tok.value);
|
|
int8_t adjust = 0;
|
|
|
|
tok = lexer.peek_token();
|
|
lexer_t::token_t::kind_t kind = tok.kind;
|
|
switch (kind) {
|
|
case lexer_t::token_t::TOK_YEAR:
|
|
case lexer_t::token_t::TOK_YEARS:
|
|
case lexer_t::token_t::TOK_QUARTER:
|
|
case lexer_t::token_t::TOK_QUARTERS:
|
|
case lexer_t::token_t::TOK_MONTH:
|
|
case lexer_t::token_t::TOK_MONTHS:
|
|
case lexer_t::token_t::TOK_WEEK:
|
|
case lexer_t::token_t::TOK_WEEKS:
|
|
case lexer_t::token_t::TOK_DAY:
|
|
case lexer_t::token_t::TOK_DAYS:
|
|
lexer.next_token();
|
|
tok = lexer.next_token();
|
|
switch (tok.kind) {
|
|
case lexer_t::token_t::TOK_AGO:
|
|
adjust = -1;
|
|
break;
|
|
case lexer_t::token_t::TOK_HENCE:
|
|
adjust = 1;
|
|
break;
|
|
default:
|
|
tok.unexpected();
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
date_t when(today);
|
|
|
|
switch (kind) {
|
|
case lexer_t::token_t::TOK_YEAR:
|
|
case lexer_t::token_t::TOK_YEARS:
|
|
when += gregorian::years(amount * adjust);
|
|
break;
|
|
case lexer_t::token_t::TOK_QUARTER:
|
|
case lexer_t::token_t::TOK_QUARTERS:
|
|
when += gregorian::months(amount * 3 * adjust);
|
|
break;
|
|
case lexer_t::token_t::TOK_MONTH:
|
|
case lexer_t::token_t::TOK_MONTHS:
|
|
when += gregorian::months(amount * adjust);
|
|
break;
|
|
case lexer_t::token_t::TOK_WEEK:
|
|
case lexer_t::token_t::TOK_WEEKS:
|
|
when += gregorian::weeks(amount * adjust);
|
|
break;
|
|
case lexer_t::token_t::TOK_DAY:
|
|
case lexer_t::token_t::TOK_DAYS:
|
|
when += gregorian::days(amount * adjust);
|
|
break;
|
|
default:
|
|
specifier.day = date_specifier_t::day_type(amount);
|
|
break;
|
|
}
|
|
|
|
if (adjust)
|
|
specifier = date_specifier_t(when);
|
|
break;
|
|
}
|
|
|
|
case lexer_t::token_t::TOK_THIS:
|
|
case lexer_t::token_t::TOK_NEXT:
|
|
case lexer_t::token_t::TOK_LAST: {
|
|
int8_t adjust = 0;
|
|
if (tok.kind == lexer_t::token_t::TOK_NEXT)
|
|
adjust = 1;
|
|
else if (tok.kind == lexer_t::token_t::TOK_LAST)
|
|
adjust = -1;
|
|
|
|
tok = lexer.next_token();
|
|
switch (tok.kind) {
|
|
case lexer_t::token_t::TOK_A_MONTH: {
|
|
date_t temp(today.year(),
|
|
boost::get<date_time::months_of_year>(*tok.value), 1);
|
|
temp += gregorian::years(adjust);
|
|
specifier =
|
|
date_specifier_t(static_cast<date_specifier_t::year_type>(temp.year()),
|
|
temp.month());
|
|
break;
|
|
}
|
|
|
|
case lexer_t::token_t::TOK_A_WDAY: {
|
|
date_t temp =
|
|
date_duration_t::find_nearest(today, date_duration_t::WEEKS);
|
|
while (temp.day_of_week() !=
|
|
boost::get<date_time::months_of_year>(*tok.value))
|
|
temp += gregorian::days(1);
|
|
temp += gregorian::days(7 * adjust);
|
|
specifier = date_specifier_t(temp);
|
|
break;
|
|
}
|
|
|
|
case lexer_t::token_t::TOK_YEAR: {
|
|
date_t temp(today);
|
|
temp += gregorian::years(adjust);
|
|
specifier =
|
|
date_specifier_t(static_cast<date_specifier_t::year_type>(temp.year()));
|
|
break;
|
|
}
|
|
|
|
case lexer_t::token_t::TOK_QUARTER: {
|
|
date_t base =
|
|
date_duration_t::find_nearest(today, date_duration_t::QUARTERS);
|
|
date_t temp;
|
|
if (adjust < 0) {
|
|
temp = base + gregorian::months(3 * adjust);
|
|
}
|
|
else if (adjust == 0) {
|
|
temp = base + gregorian::months(3);
|
|
}
|
|
else if (adjust > 0) {
|
|
base += gregorian::months(3 * adjust);
|
|
temp = base + gregorian::months(3 * adjust);
|
|
}
|
|
specifier = date_specifier_t(adjust < 0 ? temp : base);
|
|
break;
|
|
}
|
|
|
|
case lexer_t::token_t::TOK_WEEK: {
|
|
date_t base =
|
|
date_duration_t::find_nearest(today, date_duration_t::WEEKS);
|
|
date_t temp;
|
|
if (adjust < 0) {
|
|
temp = base + gregorian::days(7 * adjust);
|
|
}
|
|
else if (adjust == 0) {
|
|
temp = base + gregorian::days(7);
|
|
}
|
|
else if (adjust > 0) {
|
|
base += gregorian::days(7 * adjust);
|
|
temp = base + gregorian::days(7 * adjust);
|
|
}
|
|
specifier = date_specifier_t(adjust < 0 ? temp : base);
|
|
break;
|
|
}
|
|
|
|
case lexer_t::token_t::TOK_DAY: {
|
|
date_t temp(today);
|
|
temp += gregorian::days(adjust);
|
|
specifier = date_specifier_t(temp);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
case lexer_t::token_t::TOK_MONTH: {
|
|
date_t temp(today);
|
|
temp += gregorian::months(adjust);
|
|
specifier =
|
|
date_specifier_t(static_cast<date_specifier_t::year_type>(temp.year()),
|
|
temp.month());
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case lexer_t::token_t::TOK_A_YEAR:
|
|
specifier.year = boost::get<date_specifier_t::year_type>(*tok.value);
|
|
break;
|
|
case lexer_t::token_t::TOK_A_MONTH:
|
|
specifier.month =
|
|
date_specifier_t::month_type
|
|
(boost::get<date_time::months_of_year>(*tok.value));
|
|
break;
|
|
case lexer_t::token_t::TOK_A_WDAY:
|
|
specifier.wday =
|
|
date_specifier_t::day_of_week_type
|
|
(boost::get<date_time::weekdays>(*tok.value));
|
|
break;
|
|
|
|
case lexer_t::token_t::TOK_TODAY:
|
|
specifier = date_specifier_t(today);
|
|
break;
|
|
case lexer_t::token_t::TOK_TOMORROW:
|
|
specifier = date_specifier_t(today + gregorian::days(1));
|
|
break;
|
|
case lexer_t::token_t::TOK_YESTERDAY:
|
|
specifier = date_specifier_t(today - gregorian::days(1));
|
|
break;
|
|
|
|
default:
|
|
tok.unexpected();
|
|
break;
|
|
}
|
|
}
|
|
|
|
date_interval_t date_parser_t::parse()
|
|
{
|
|
optional<date_specifier_t> since_specifier;
|
|
optional<date_specifier_t> until_specifier;
|
|
optional<date_specifier_t> inclusion_specifier;
|
|
|
|
date_interval_t period;
|
|
date_t today = CURRENT_DATE();
|
|
bool end_inclusive = false;
|
|
|
|
for (lexer_t::token_t tok = lexer.next_token();
|
|
tok.kind != lexer_t::token_t::END_REACHED;
|
|
tok = lexer.next_token()) {
|
|
switch (tok.kind) {
|
|
case lexer_t::token_t::TOK_DATE:
|
|
if (! inclusion_specifier)
|
|
inclusion_specifier = date_specifier_t();
|
|
determine_when(tok, *inclusion_specifier);
|
|
break;
|
|
|
|
case lexer_t::token_t::TOK_INT:
|
|
if (! inclusion_specifier)
|
|
inclusion_specifier = date_specifier_t();
|
|
determine_when(tok, *inclusion_specifier);
|
|
break;
|
|
|
|
case lexer_t::token_t::TOK_A_YEAR:
|
|
if (! inclusion_specifier)
|
|
inclusion_specifier = date_specifier_t();
|
|
determine_when(tok, *inclusion_specifier);
|
|
break;
|
|
|
|
case lexer_t::token_t::TOK_A_MONTH:
|
|
if (! inclusion_specifier)
|
|
inclusion_specifier = date_specifier_t();
|
|
determine_when(tok, *inclusion_specifier);
|
|
break;
|
|
|
|
case lexer_t::token_t::TOK_A_WDAY:
|
|
if (! inclusion_specifier)
|
|
inclusion_specifier = date_specifier_t();
|
|
determine_when(tok, *inclusion_specifier);
|
|
break;
|
|
|
|
case lexer_t::token_t::TOK_DASH:
|
|
if (inclusion_specifier) {
|
|
since_specifier = inclusion_specifier;
|
|
until_specifier = date_specifier_t();
|
|
inclusion_specifier = none;
|
|
|
|
tok = lexer.next_token();
|
|
determine_when(tok, *until_specifier);
|
|
|
|
// The dash operator is special: it has an _inclusive_ end.
|
|
end_inclusive = true;
|
|
} else {
|
|
tok.unexpected();
|
|
}
|
|
break;
|
|
|
|
case lexer_t::token_t::TOK_SINCE:
|
|
if (since_specifier) {
|
|
tok.unexpected();
|
|
} else {
|
|
since_specifier = date_specifier_t();
|
|
tok = lexer.next_token();
|
|
determine_when(tok, *since_specifier);
|
|
}
|
|
break;
|
|
|
|
case lexer_t::token_t::TOK_UNTIL:
|
|
if (until_specifier) {
|
|
tok.unexpected();
|
|
} else {
|
|
until_specifier = date_specifier_t();
|
|
tok = lexer.next_token();
|
|
determine_when(tok, *until_specifier);
|
|
}
|
|
break;
|
|
|
|
case lexer_t::token_t::TOK_IN:
|
|
if (inclusion_specifier) {
|
|
tok.unexpected();
|
|
} else {
|
|
inclusion_specifier = date_specifier_t();
|
|
tok = lexer.next_token();
|
|
determine_when(tok, *inclusion_specifier);
|
|
}
|
|
break;
|
|
|
|
case lexer_t::token_t::TOK_THIS:
|
|
case lexer_t::token_t::TOK_NEXT:
|
|
case lexer_t::token_t::TOK_LAST: {
|
|
int8_t adjust = 0;
|
|
if (tok.kind == lexer_t::token_t::TOK_NEXT)
|
|
adjust = 1;
|
|
else if (tok.kind == lexer_t::token_t::TOK_LAST)
|
|
adjust = -1;
|
|
|
|
tok = lexer.next_token();
|
|
switch (tok.kind) {
|
|
case lexer_t::token_t::TOK_INT: {
|
|
unsigned short amount = boost::get<unsigned short>(*tok.value);
|
|
|
|
date_t base(today);
|
|
date_t end(today);
|
|
|
|
if (! adjust)
|
|
adjust = 1;
|
|
|
|
tok = lexer.next_token();
|
|
switch (tok.kind) {
|
|
case lexer_t::token_t::TOK_YEARS:
|
|
base += gregorian::years(amount * adjust);
|
|
break;
|
|
case lexer_t::token_t::TOK_QUARTERS:
|
|
base += gregorian::months(amount * adjust * 3);
|
|
break;
|
|
case lexer_t::token_t::TOK_MONTHS:
|
|
base += gregorian::months(amount * adjust);
|
|
break;
|
|
case lexer_t::token_t::TOK_WEEKS:
|
|
base += gregorian::weeks(amount * adjust);
|
|
break;
|
|
case lexer_t::token_t::TOK_DAYS:
|
|
base += gregorian::days(amount * adjust);
|
|
break;
|
|
default:
|
|
tok.unexpected();
|
|
break;
|
|
}
|
|
|
|
if (adjust >= 0) {
|
|
date_t temp = base;
|
|
base = end;
|
|
end = temp;
|
|
}
|
|
|
|
since_specifier = date_specifier_t(base);
|
|
until_specifier = date_specifier_t(end);
|
|
break;
|
|
}
|
|
|
|
case lexer_t::token_t::TOK_A_MONTH: {
|
|
inclusion_specifier = date_specifier_t();
|
|
determine_when(tok, *inclusion_specifier);
|
|
|
|
date_t temp(today.year(), *inclusion_specifier->month, 1);
|
|
temp += gregorian::years(adjust);
|
|
inclusion_specifier =
|
|
date_specifier_t(static_cast<date_specifier_t::year_type>(temp.year()),
|
|
temp.month());
|
|
break;
|
|
}
|
|
|
|
case lexer_t::token_t::TOK_A_WDAY: {
|
|
inclusion_specifier = date_specifier_t();
|
|
determine_when(tok, *inclusion_specifier);
|
|
|
|
date_t temp =
|
|
date_duration_t::find_nearest(today, date_duration_t::WEEKS);
|
|
while (temp.day_of_week() != inclusion_specifier->wday)
|
|
temp += gregorian::days(1);
|
|
temp += gregorian::days(7 * adjust);
|
|
inclusion_specifier = date_specifier_t(temp);
|
|
break;
|
|
}
|
|
|
|
case lexer_t::token_t::TOK_YEAR: {
|
|
date_t temp(today);
|
|
temp += gregorian::years(adjust);
|
|
inclusion_specifier =
|
|
date_specifier_t(static_cast<date_specifier_t::year_type>(temp.year()));
|
|
break;
|
|
}
|
|
|
|
case lexer_t::token_t::TOK_QUARTER: {
|
|
date_t base =
|
|
date_duration_t::find_nearest(today, date_duration_t::QUARTERS);
|
|
date_t temp;
|
|
if (adjust < 0) {
|
|
temp = base + gregorian::months(3 * adjust);
|
|
}
|
|
else if (adjust == 0) {
|
|
temp = base + gregorian::months(3);
|
|
}
|
|
else if (adjust > 0) {
|
|
base += gregorian::months(3 * adjust);
|
|
temp = base + gregorian::months(3 * adjust);
|
|
}
|
|
since_specifier = date_specifier_t(adjust < 0 ? temp : base);
|
|
until_specifier = date_specifier_t(adjust < 0 ? base : temp);
|
|
break;
|
|
}
|
|
|
|
case lexer_t::token_t::TOK_WEEK: {
|
|
date_t base =
|
|
date_duration_t::find_nearest(today, date_duration_t::WEEKS);
|
|
date_t temp;
|
|
if (adjust < 0) {
|
|
temp = base + gregorian::days(7 * adjust);
|
|
}
|
|
else if (adjust == 0) {
|
|
temp = base + gregorian::days(7);
|
|
}
|
|
else if (adjust > 0) {
|
|
base += gregorian::days(7 * adjust);
|
|
temp = base + gregorian::days(7 * adjust);
|
|
}
|
|
since_specifier = date_specifier_t(adjust < 0 ? temp : base);
|
|
until_specifier = date_specifier_t(adjust < 0 ? base : temp);
|
|
break;
|
|
}
|
|
|
|
case lexer_t::token_t::TOK_DAY: {
|
|
date_t temp(today);
|
|
temp += gregorian::days(adjust);
|
|
inclusion_specifier = date_specifier_t(temp);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
case lexer_t::token_t::TOK_MONTH: {
|
|
date_t temp(today);
|
|
temp += gregorian::months(adjust);
|
|
inclusion_specifier =
|
|
date_specifier_t(static_cast<date_specifier_t::year_type>(temp.year()),
|
|
temp.month());
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case lexer_t::token_t::TOK_TODAY:
|
|
inclusion_specifier = date_specifier_t(today);
|
|
break;
|
|
case lexer_t::token_t::TOK_TOMORROW:
|
|
inclusion_specifier = date_specifier_t(today + gregorian::days(1));
|
|
break;
|
|
case lexer_t::token_t::TOK_YESTERDAY:
|
|
inclusion_specifier = date_specifier_t(today - gregorian::days(1));
|
|
break;
|
|
|
|
case lexer_t::token_t::TOK_EVERY:
|
|
tok = lexer.next_token();
|
|
if (tok.kind == lexer_t::token_t::TOK_INT) {
|
|
int quantity = boost::get<unsigned short>(*tok.value);
|
|
tok = lexer.next_token();
|
|
switch (tok.kind) {
|
|
case lexer_t::token_t::TOK_YEARS:
|
|
period.duration = date_duration_t(date_duration_t::YEARS, quantity);
|
|
break;
|
|
case lexer_t::token_t::TOK_QUARTERS:
|
|
period.duration = date_duration_t(date_duration_t::QUARTERS, quantity);
|
|
break;
|
|
case lexer_t::token_t::TOK_MONTHS:
|
|
period.duration = date_duration_t(date_duration_t::MONTHS, quantity);
|
|
break;
|
|
case lexer_t::token_t::TOK_WEEKS:
|
|
period.duration = date_duration_t(date_duration_t::WEEKS, quantity);
|
|
break;
|
|
case lexer_t::token_t::TOK_DAYS:
|
|
period.duration = date_duration_t(date_duration_t::DAYS, quantity);
|
|
break;
|
|
default:
|
|
tok.unexpected();
|
|
break;
|
|
}
|
|
} else {
|
|
switch (tok.kind) {
|
|
case lexer_t::token_t::TOK_YEAR:
|
|
period.duration = date_duration_t(date_duration_t::YEARS, 1);
|
|
break;
|
|
case lexer_t::token_t::TOK_QUARTER:
|
|
period.duration = date_duration_t(date_duration_t::QUARTERS, 1);
|
|
break;
|
|
case lexer_t::token_t::TOK_MONTH:
|
|
period.duration = date_duration_t(date_duration_t::MONTHS, 1);
|
|
break;
|
|
case lexer_t::token_t::TOK_WEEK:
|
|
period.duration = date_duration_t(date_duration_t::WEEKS, 1);
|
|
break;
|
|
case lexer_t::token_t::TOK_DAY:
|
|
period.duration = date_duration_t(date_duration_t::DAYS, 1);
|
|
break;
|
|
default:
|
|
tok.unexpected();
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case lexer_t::token_t::TOK_YEARLY:
|
|
period.duration = date_duration_t(date_duration_t::YEARS, 1);
|
|
break;
|
|
case lexer_t::token_t::TOK_QUARTERLY:
|
|
period.duration = date_duration_t(date_duration_t::QUARTERS, 1);
|
|
break;
|
|
case lexer_t::token_t::TOK_BIMONTHLY:
|
|
period.duration = date_duration_t(date_duration_t::MONTHS, 2);
|
|
break;
|
|
case lexer_t::token_t::TOK_MONTHLY:
|
|
period.duration = date_duration_t(date_duration_t::MONTHS, 1);
|
|
break;
|
|
case lexer_t::token_t::TOK_BIWEEKLY:
|
|
period.duration = date_duration_t(date_duration_t::WEEKS, 2);
|
|
break;
|
|
case lexer_t::token_t::TOK_WEEKLY:
|
|
period.duration = date_duration_t(date_duration_t::WEEKS, 1);
|
|
break;
|
|
case lexer_t::token_t::TOK_DAILY:
|
|
period.duration = date_duration_t(date_duration_t::DAYS, 1);
|
|
break;
|
|
|
|
default:
|
|
tok.unexpected();
|
|
break;
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
if (! period.duration && inclusion_specifier)
|
|
period.duration = inclusion_specifier->implied_duration();
|
|
#endif
|
|
|
|
if (since_specifier || until_specifier) {
|
|
date_range_t range(since_specifier, until_specifier);
|
|
range.end_inclusive = end_inclusive;
|
|
|
|
period.range = date_specifier_or_range_t(range);
|
|
}
|
|
else if (inclusion_specifier) {
|
|
period.range = date_specifier_or_range_t(*inclusion_specifier);
|
|
}
|
|
else {
|
|
/* otherwise, it's something like "monthly", with no date reference */
|
|
}
|
|
|
|
return period;
|
|
}
|
|
|
|
void date_interval_t::parse(const string& str)
|
|
{
|
|
date_parser_t parser(str);
|
|
*this = parser.parse();
|
|
}
|
|
|
|
void date_interval_t::resolve_end()
|
|
{
|
|
if (start && ! end_of_duration) {
|
|
end_of_duration = duration->add(*start);
|
|
DEBUG("times.interval",
|
|
"stabilize: end_of_duration = " << *end_of_duration);
|
|
}
|
|
|
|
if (finish && *end_of_duration > *finish) {
|
|
end_of_duration = finish;
|
|
DEBUG("times.interval",
|
|
"stabilize: end_of_duration reset to end: " << *end_of_duration);
|
|
}
|
|
|
|
if (start && ! next) {
|
|
next = end_of_duration;
|
|
DEBUG("times.interval", "stabilize: next set to: " << *next);
|
|
}
|
|
}
|
|
|
|
date_t date_duration_t::find_nearest(const date_t& date, skip_quantum_t skip)
|
|
{
|
|
date_t result;
|
|
|
|
switch (skip) {
|
|
case date_duration_t::YEARS:
|
|
result = date_t(date.year(), gregorian::Jan, 1);
|
|
break;
|
|
case date_duration_t::QUARTERS:
|
|
result = date_t(date.year(), date.month(), 1);
|
|
while (result.month() != gregorian::Jan &&
|
|
result.month() != gregorian::Apr &&
|
|
result.month() != gregorian::Jul &&
|
|
result.month() != gregorian::Oct)
|
|
result -= gregorian::months(1);
|
|
break;
|
|
case date_duration_t::MONTHS:
|
|
result = date_t(date.year(), date.month(), 1);
|
|
break;
|
|
case date_duration_t::WEEKS:
|
|
result = date;
|
|
while (result.day_of_week() != start_of_week)
|
|
result -= gregorian::days(1);
|
|
break;
|
|
case date_duration_t::DAYS:
|
|
result = date;
|
|
break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void date_interval_t::stabilize(const optional<date_t>& date)
|
|
{
|
|
#if defined(DEBUG_ON)
|
|
if (date)
|
|
DEBUG("times.interval", "stabilize: with date = " << *date);
|
|
#endif
|
|
|
|
if (date && ! aligned) {
|
|
DEBUG("times.interval", "stabilize: date passed, but not aligned");
|
|
if (duration) {
|
|
DEBUG("times.interval",
|
|
"stabilize: aligning with a duration: " << *duration);
|
|
|
|
// The interval object has not been seeded with a start date yet, so
|
|
// find the nearest period before on on date which fits, if possible.
|
|
//
|
|
// Find an efficient starting point for the upcoming while loop. We
|
|
// want a date early enough that the range will be correct, but late
|
|
// enough that we don't spend hundreds of thousands of loops skipping
|
|
// through time.
|
|
optional<date_t> initial_start = start ? start : begin();
|
|
optional<date_t> initial_finish = finish ? finish : end();
|
|
|
|
#if defined(DEBUG_ON)
|
|
if (initial_start)
|
|
DEBUG("times.interval",
|
|
"stabilize: initial_start = " << *initial_start);
|
|
if (initial_finish)
|
|
DEBUG("times.interval",
|
|
"stabilize: initial_finish = " << *initial_finish);
|
|
#endif
|
|
|
|
date_t when = start ? *start : *date;
|
|
|
|
if (duration->quantum == date_duration_t::MONTHS ||
|
|
duration->quantum == date_duration_t::QUARTERS ||
|
|
duration->quantum == date_duration_t::YEARS) {
|
|
DEBUG("times.interval",
|
|
"stabilize: monthly, quarterly or yearly duration");
|
|
start = date_duration_t::find_nearest(when, duration->quantum);
|
|
} else {
|
|
DEBUG("times.interval", "stabilize: daily or weekly duration");
|
|
start = date_duration_t::find_nearest(when - gregorian::days(400),
|
|
duration->quantum);
|
|
}
|
|
|
|
DEBUG("times.interval",
|
|
"stabilize: beginning start date = " << *start);
|
|
|
|
while (*start < *date) {
|
|
date_interval_t next_interval(*this);
|
|
++next_interval;
|
|
|
|
if (next_interval.start && *next_interval.start <= *date) {
|
|
*this = next_interval;
|
|
} else {
|
|
end_of_duration = none;
|
|
next = none;
|
|
break;
|
|
}
|
|
}
|
|
|
|
DEBUG("times.interval", "stabilize: proposed start date = " << *start);
|
|
|
|
if (initial_start && (! start || *start < *initial_start)) {
|
|
// Using the discovered start, find the end of the period
|
|
resolve_end();
|
|
|
|
start = initial_start;
|
|
DEBUG("times.interval", "stabilize: start reset to initial start");
|
|
}
|
|
if (initial_finish && (! finish || *finish > *initial_finish)) {
|
|
finish = initial_finish;
|
|
DEBUG("times.interval", "stabilize: finish reset to initial finish");
|
|
}
|
|
|
|
#if defined(DEBUG_ON)
|
|
if (start)
|
|
DEBUG("times.interval", "stabilize: final start = " << *start);
|
|
if (finish)
|
|
DEBUG("times.interval", "stabilize: final finish = " << *finish);
|
|
#endif
|
|
}
|
|
else if (range) {
|
|
start = range->begin();
|
|
finish = range->end();
|
|
}
|
|
aligned = true;
|
|
}
|
|
|
|
// If there is no duration, then if we've reached here the date falls
|
|
// between start and finish.
|
|
if (! duration) {
|
|
DEBUG("times.interval", "stabilize: there was no duration given");
|
|
|
|
if (! start && ! finish)
|
|
throw_(date_error,
|
|
_("Invalid date interval: neither start, nor finish, nor duration"));
|
|
} else {
|
|
resolve_end();
|
|
}
|
|
}
|
|
|
|
bool date_interval_t::find_period(const date_t& date,
|
|
const bool allow_shift)
|
|
{
|
|
stabilize(date);
|
|
|
|
if (finish && date > *finish) {
|
|
DEBUG("times.interval",
|
|
"false: date [" << date << "] > finish [" << *finish << "]");
|
|
return false;
|
|
}
|
|
|
|
if (! start) {
|
|
throw_(std::runtime_error, _("Date interval is improperly initialized"));
|
|
}
|
|
else if (date < *start) {
|
|
DEBUG("times.interval",
|
|
"false: date [" << date << "] < start [" << *start << "]");
|
|
return false;
|
|
}
|
|
|
|
if (end_of_duration) {
|
|
if (date < *end_of_duration) {
|
|
DEBUG("times.interval",
|
|
"true: date [" << date << "] < end_of_duration ["
|
|
<< *end_of_duration << "]");
|
|
return true;
|
|
}
|
|
} else {
|
|
DEBUG("times.interval", "false: there is no end_of_duration");
|
|
return false;
|
|
}
|
|
|
|
// If we've reached here, it means the date does not fall into the current
|
|
// interval, so we must seek another interval that does match -- unless we
|
|
// pass by date in so doing, which means we shouldn't alter the current
|
|
// period of the interval at all.
|
|
|
|
date_t scan = *start;
|
|
date_t end_of_scan = *end_of_duration;
|
|
|
|
DEBUG("times.interval", "date = " << date);
|
|
DEBUG("times.interval", "scan = " << scan);
|
|
DEBUG("times.interval", "end_of_scan = " << end_of_scan);
|
|
#if defined(DEBUG_ON)
|
|
if (finish)
|
|
DEBUG("times.interval", "finish = " << *finish);
|
|
else
|
|
DEBUG("times.interval", "finish is not set");
|
|
#endif
|
|
|
|
while (date >= scan && (! finish || scan < *finish)) {
|
|
if (date < end_of_scan) {
|
|
start = scan;
|
|
end_of_duration = end_of_scan;
|
|
next = none;
|
|
|
|
DEBUG("times.interval", "true: start = " << *start);
|
|
DEBUG("times.interval", "true: end_of_duration = " << *end_of_duration);
|
|
|
|
resolve_end();
|
|
|
|
return true;
|
|
}
|
|
else if (! allow_shift) {
|
|
break;
|
|
}
|
|
|
|
scan = duration->add(scan);
|
|
end_of_scan = duration->add(scan);
|
|
|
|
DEBUG("times.interval", "scan = " << scan);
|
|
DEBUG("times.interval", "end_of_scan = " << end_of_scan);
|
|
}
|
|
|
|
DEBUG("times.interval", "false: failed scan");
|
|
|
|
return false;
|
|
}
|
|
|
|
date_interval_t& date_interval_t::operator++()
|
|
{
|
|
if (! start)
|
|
throw_(date_error, _("Cannot increment an unstarted date interval"));
|
|
|
|
stabilize();
|
|
|
|
if (! duration)
|
|
throw_(date_error,
|
|
_("Cannot increment a date interval without a duration"));
|
|
|
|
assert(next);
|
|
|
|
if (finish && *next >= *finish) {
|
|
start = none;
|
|
} else {
|
|
start = *next;
|
|
end_of_duration = duration->add(*start);
|
|
}
|
|
next = none;
|
|
|
|
resolve_end();
|
|
|
|
return *this;
|
|
}
|
|
|
|
void date_interval_t::dump(std::ostream& out)
|
|
{
|
|
out << _("--- Before stabilization ---") << std::endl;
|
|
|
|
if (range)
|
|
out << _(" range: ") << range->to_string() << std::endl;
|
|
if (start)
|
|
out << _(" start: ") << format_date(*start) << std::endl;
|
|
if (finish)
|
|
out << _(" finish: ") << format_date(*finish) << std::endl;
|
|
|
|
if (duration)
|
|
out << _("duration: ") << duration->to_string() << std::endl;
|
|
|
|
optional<date_t> when(begin());
|
|
if (! when)
|
|
when = CURRENT_DATE();
|
|
|
|
stabilize(when);
|
|
|
|
out << std::endl
|
|
<< _("--- After stabilization ---") << std::endl;
|
|
|
|
if (range)
|
|
out << _(" range: ") << range->to_string() << std::endl;
|
|
if (start)
|
|
out << _(" start: ") << format_date(*start) << std::endl;
|
|
if (finish)
|
|
out << _(" finish: ") << format_date(*finish) << std::endl;
|
|
|
|
if (duration)
|
|
out << _("duration: ") << duration->to_string() << std::endl;
|
|
|
|
out << std::endl
|
|
<< _("--- Sample dates in range (max. 20) ---") << std::endl;
|
|
|
|
date_t last_date;
|
|
|
|
for (int i = 0; i < 20 && *this; ++i, ++*this) {
|
|
out << std::right;
|
|
out.width(2);
|
|
|
|
if (! last_date.is_not_a_date() && last_date == *start)
|
|
break;
|
|
|
|
out << (i + 1) << ": " << format_date(*start);
|
|
if (duration)
|
|
out << " -- " << format_date(*inclusive_end());
|
|
out << std::endl;
|
|
|
|
if (! duration)
|
|
break;
|
|
|
|
last_date = *start;
|
|
}
|
|
}
|
|
|
|
date_parser_t::lexer_t::token_t date_parser_t::lexer_t::next_token()
|
|
{
|
|
if (token_cache.kind != token_t::UNKNOWN) {
|
|
token_t tok = token_cache;
|
|
token_cache = token_t();
|
|
return tok;
|
|
}
|
|
|
|
while (begin != end && std::isspace(*begin))
|
|
begin++;
|
|
|
|
if (begin == end)
|
|
return token_t(token_t::END_REACHED);
|
|
|
|
switch (*begin) {
|
|
case '/': ++begin; return token_t(token_t::TOK_SLASH);
|
|
case '-': ++begin; return token_t(token_t::TOK_DASH);
|
|
case '.': ++begin; return token_t(token_t::TOK_DOT);
|
|
default: break;
|
|
}
|
|
|
|
string::const_iterator start = begin;
|
|
|
|
// If the first character is a digit, try parsing the whole argument as a
|
|
// date using the typical date formats. This allows not only dates like
|
|
// "2009/08/01", but also dates that fit the user's --input-date-format,
|
|
// assuming their format fits in one argument and begins with a digit.
|
|
if (std::isdigit(*begin)) {
|
|
string::const_iterator i = begin;
|
|
for (i = begin; i != end && ! std::isspace(*i); i++) {}
|
|
assert(i != begin);
|
|
|
|
string possible_date(start, i);
|
|
|
|
try {
|
|
date_traits_t traits;
|
|
date_t when = parse_date_mask(possible_date.c_str(), &traits);
|
|
if (! when.is_not_a_date()) {
|
|
begin = i;
|
|
return token_t(token_t::TOK_DATE,
|
|
token_t::content_t(date_specifier_t(when, traits)));
|
|
}
|
|
}
|
|
catch (date_error&) {
|
|
if (contains(possible_date, "/") ||
|
|
contains(possible_date, "-") ||
|
|
contains(possible_date, "."))
|
|
throw;
|
|
}
|
|
}
|
|
|
|
start = begin;
|
|
|
|
string term;
|
|
bool alnum = std::isalnum(*begin);
|
|
for (; (begin != end && ! std::isspace(*begin) &&
|
|
((alnum && static_cast<bool>(std::isalnum(*begin))) ||
|
|
(! alnum && ! static_cast<bool>(std::isalnum(*begin))))); begin++)
|
|
term.push_back(*begin);
|
|
|
|
if (! term.empty()) {
|
|
if (std::isdigit(term[0])) {
|
|
if (term.length() == 4)
|
|
return token_t(token_t::TOK_A_YEAR,
|
|
token_t::content_t
|
|
(lexical_cast<date_specifier_t::year_type>(term)));
|
|
else
|
|
return token_t(token_t::TOK_INT,
|
|
token_t::content_t(lexical_cast<unsigned short>(term)));
|
|
}
|
|
else if (std::isalpha(term[0])) {
|
|
to_lower(term);
|
|
|
|
if (optional<date_time::months_of_year> month =
|
|
string_to_month_of_year(term)) {
|
|
return token_t(token_t::TOK_A_MONTH, token_t::content_t(*month));
|
|
}
|
|
else if (optional<date_time::weekdays> wday =
|
|
string_to_day_of_week(term)) {
|
|
return token_t(token_t::TOK_A_WDAY, token_t::content_t(*wday));
|
|
}
|
|
else if (term == _("ago"))
|
|
return token_t(token_t::TOK_AGO);
|
|
else if (term == _("hence"))
|
|
return token_t(token_t::TOK_HENCE);
|
|
else if (term == _("from") || term == _("since"))
|
|
return token_t(token_t::TOK_SINCE);
|
|
else if (term == _("to") || term == _("until"))
|
|
return token_t(token_t::TOK_UNTIL);
|
|
else if (term == _("in"))
|
|
return token_t(token_t::TOK_IN);
|
|
else if (term == _("this"))
|
|
return token_t(token_t::TOK_THIS);
|
|
else if (term == _("next"))
|
|
return token_t(token_t::TOK_NEXT);
|
|
else if (term == _("last"))
|
|
return token_t(token_t::TOK_LAST);
|
|
else if (term == _("every"))
|
|
return token_t(token_t::TOK_EVERY);
|
|
else if (term == _("today"))
|
|
return token_t(token_t::TOK_TODAY);
|
|
else if (term == _("tomorrow"))
|
|
return token_t(token_t::TOK_TOMORROW);
|
|
else if (term == _("yesterday"))
|
|
return token_t(token_t::TOK_YESTERDAY);
|
|
else if (term == _("year"))
|
|
return token_t(token_t::TOK_YEAR);
|
|
else if (term == _("quarter"))
|
|
return token_t(token_t::TOK_QUARTER);
|
|
else if (term == _("month"))
|
|
return token_t(token_t::TOK_MONTH);
|
|
else if (term == _("week"))
|
|
return token_t(token_t::TOK_WEEK);
|
|
else if (term == _("day"))
|
|
return token_t(token_t::TOK_DAY);
|
|
else if (term == _("yearly"))
|
|
return token_t(token_t::TOK_YEARLY);
|
|
else if (term == _("quarterly"))
|
|
return token_t(token_t::TOK_QUARTERLY);
|
|
else if (term == _("bimonthly"))
|
|
return token_t(token_t::TOK_BIMONTHLY);
|
|
else if (term == _("monthly"))
|
|
return token_t(token_t::TOK_MONTHLY);
|
|
else if (term == _("biweekly"))
|
|
return token_t(token_t::TOK_BIWEEKLY);
|
|
else if (term == _("weekly"))
|
|
return token_t(token_t::TOK_WEEKLY);
|
|
else if (term == _("daily"))
|
|
return token_t(token_t::TOK_DAILY);
|
|
else if (term == _("years"))
|
|
return token_t(token_t::TOK_YEARS);
|
|
else if (term == _("quarters"))
|
|
return token_t(token_t::TOK_QUARTERS);
|
|
else if (term == _("months"))
|
|
return token_t(token_t::TOK_MONTHS);
|
|
else if (term == _("weeks"))
|
|
return token_t(token_t::TOK_WEEKS);
|
|
else if (term == _("days"))
|
|
return token_t(token_t::TOK_DAYS);
|
|
}
|
|
else {
|
|
token_t::expected('\0', term[0]);
|
|
begin = ++start;
|
|
}
|
|
} else {
|
|
token_t::expected('\0', *begin);
|
|
}
|
|
|
|
return token_t(token_t::UNKNOWN, token_t::content_t(term));
|
|
}
|
|
|
|
void date_parser_t::lexer_t::token_t::unexpected()
|
|
{
|
|
switch (kind) {
|
|
case END_REACHED:
|
|
kind = UNKNOWN;
|
|
throw_(date_error, _("Unexpected end of expression"));
|
|
default: {
|
|
string desc = to_string();
|
|
kind = UNKNOWN;
|
|
throw_(date_error, _("Unexpected date period token '%1'") << desc);
|
|
}
|
|
}
|
|
}
|
|
|
|
void date_parser_t::lexer_t::token_t::expected(char wanted, char c)
|
|
{
|
|
if (c == '\0' || c == -1) {
|
|
if (wanted == '\0' || wanted == -1)
|
|
throw_(date_error, _("Unexpected end"));
|
|
else
|
|
throw_(date_error, _("Missing '%1'") << wanted);
|
|
} else {
|
|
if (wanted == '\0' || wanted == -1)
|
|
throw_(date_error, _("Invalid char '%1'") << c);
|
|
else
|
|
throw_(date_error, _("Invalid char '%1' (wanted '%2')") << c << wanted);
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
typedef std::map<std::string, datetime_io_t *> datetime_io_map;
|
|
typedef std::map<std::string, date_io_t *> date_io_map;
|
|
|
|
datetime_io_map temp_datetime_io;
|
|
date_io_map temp_date_io;
|
|
}
|
|
|
|
std::string format_datetime(const datetime_t& when,
|
|
const format_type_t format_type,
|
|
const optional<const char *>& format)
|
|
{
|
|
if (format_type == FMT_WRITTEN) {
|
|
return written_datetime_io->format(when);
|
|
}
|
|
else if (format_type == FMT_CUSTOM && format) {
|
|
datetime_io_map::iterator i = temp_datetime_io.find(*format);
|
|
if (i != temp_datetime_io.end()) {
|
|
return (*i).second->format(when);
|
|
} else {
|
|
datetime_io_t * formatter = new datetime_io_t(*format, false);
|
|
temp_datetime_io.insert(datetime_io_map::value_type(*format, formatter));
|
|
return formatter->format(when);
|
|
}
|
|
}
|
|
else if (format_type == FMT_PRINTED) {
|
|
return printed_datetime_io->format(when);
|
|
}
|
|
else {
|
|
assert(false);
|
|
return empty_string;
|
|
}
|
|
}
|
|
|
|
std::string format_date(const date_t& when,
|
|
const format_type_t format_type,
|
|
const optional<const char *>& format)
|
|
{
|
|
if (format_type == FMT_WRITTEN) {
|
|
return written_date_io->format(when);
|
|
}
|
|
else if (format_type == FMT_CUSTOM && format) {
|
|
date_io_map::iterator i = temp_date_io.find(*format);
|
|
if (i != temp_date_io.end()) {
|
|
return (*i).second->format(when);
|
|
} else {
|
|
date_io_t * formatter = new date_io_t(*format, false);
|
|
temp_date_io.insert(date_io_map::value_type(*format, formatter));
|
|
return formatter->format(when);
|
|
}
|
|
}
|
|
else if (format_type == FMT_PRINTED) {
|
|
return printed_date_io->format(when);
|
|
}
|
|
else {
|
|
assert(false);
|
|
return empty_string;
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
bool is_initialized = false;
|
|
}
|
|
|
|
void set_datetime_format(const char * format)
|
|
{
|
|
printed_datetime_io->set_format(format);
|
|
}
|
|
|
|
void set_date_format(const char * format)
|
|
{
|
|
printed_date_io->set_format(format);
|
|
}
|
|
|
|
void set_input_date_format(const char * format)
|
|
{
|
|
readers.push_front(shared_ptr<date_io_t>(new date_io_t(format, true)));
|
|
convert_separators_to_slashes = false;
|
|
}
|
|
|
|
void times_initialize()
|
|
{
|
|
if (! is_initialized) {
|
|
input_datetime_io.reset(new datetime_io_t("%Y/%m/%d %H:%M:%S", true));
|
|
timelog_datetime_io.reset(new datetime_io_t("%m/%d/%Y %H:%M:%S", true));
|
|
|
|
written_datetime_io.reset(new datetime_io_t("%Y/%m/%d %H:%M:%S", false));
|
|
written_date_io.reset(new date_io_t("%Y/%m/%d", false));
|
|
|
|
printed_datetime_io.reset(new datetime_io_t("%y-%b-%d %H:%M:%S", false));
|
|
printed_date_io.reset(new date_io_t("%y-%b-%d", false));
|
|
|
|
readers.push_back(shared_ptr<date_io_t>(new date_io_t("%m/%d", true)));
|
|
readers.push_back(shared_ptr<date_io_t>(new date_io_t("%Y/%m/%d", true)));
|
|
readers.push_back(shared_ptr<date_io_t>(new date_io_t("%Y/%m", true)));
|
|
readers.push_back(shared_ptr<date_io_t>(new date_io_t("%y/%m/%d", true)));
|
|
|
|
is_initialized = true;
|
|
}
|
|
}
|
|
|
|
void times_shutdown()
|
|
{
|
|
if (is_initialized) {
|
|
input_datetime_io.reset();
|
|
timelog_datetime_io.reset();
|
|
input_date_io.reset();
|
|
written_datetime_io.reset();
|
|
written_date_io.reset();
|
|
printed_datetime_io.reset();
|
|
printed_date_io.reset();
|
|
|
|
readers.clear();
|
|
|
|
foreach (datetime_io_map::value_type& pair, temp_datetime_io)
|
|
checked_delete(pair.second);
|
|
temp_datetime_io.clear();
|
|
|
|
foreach (date_io_map::value_type& pair, temp_date_io)
|
|
checked_delete(pair.second);
|
|
temp_date_io.clear();
|
|
|
|
is_initialized = false;
|
|
}
|
|
}
|
|
|
|
void show_period_tokens(std::ostream& out, const string& arg)
|
|
{
|
|
date_parser_t::lexer_t lexer(arg.begin(), arg.end());
|
|
|
|
out << _("--- Period expression tokens ---") << std::endl;
|
|
|
|
date_parser_t::lexer_t::token_t token;
|
|
do {
|
|
token = lexer.next_token();
|
|
token.dump(out);
|
|
out << ": " << token.to_string() << std::endl;
|
|
}
|
|
while (token.kind != date_parser_t::lexer_t::token_t::END_REACHED);
|
|
}
|
|
|
|
} // namespace ledger
|