ledger/src/times.cc
John Wiegley 3a9b7df4e5 date_interval_t now uses a duration_t object
This is instead of using boost::variant.
2009-10-30 18:03:26 -04:00

877 lines
25 KiB
C++

/*
* Copyright (c) 2003-2009, 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
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:
bool has_year;
bool input;
temporal_io_t(const char * _fmt_str, bool _input)
: fmt_str(_fmt_str), has_year(icontains(fmt_str, "%y")),
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;
has_year = icontains(fmt_str, "%y");
#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 * str) {
}
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_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<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::vector<shared_ptr<date_io_t> > readers;
date_t parse_date_mask_routine(const char * date_str, date_io_t& io,
optional<date_t::year_type> year,
bool& saw_year)
{
date_t when;
if (std::strchr(date_str, '/')) {
when = io.parse(date_str);
} else {
char buf[128];
VERIFY(std::strlen(date_str) < 127);
std::strcpy(buf, date_str);
for (char * p = buf; *p; p++)
if (*p == '.' || *p == '-')
*p = '/';
when = io.parse(buf);
}
if (! when.is_not_a_date()) {
DEBUG("times.parse", "Parsed date string: " << date_str);
DEBUG("times.parse", "Parsed result is: " << when);
if (! io.has_year) {
saw_year = false;
when = date_t(year ? *year : CURRENT_DATE().year(),
when.month(), when.day());
if (when.month() > CURRENT_DATE().month())
when -= gregorian::years(1);
} else {
saw_year = true;
}
}
return when;
}
date_t parse_date_mask(const char * date_str,
optional<date_t::year_type> year, bool& saw_year)
{
if (input_date_io.get()) {
date_t when = parse_date_mask_routine(date_str, *input_date_io.get(),
year, saw_year);
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(),
year, saw_year);
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, optional<date_t::year_type>)
{
datetime_t when = input_datetime_io->parse(str);
if (when.is_not_a_date_time())
throw_(date_error, _("Invalid date/time: %1") << str);
return when;
}
date_t parse_date(const char * str, optional<date_t::year_type> current_year)
{
bool saw_year;
return parse_date_mask(str, current_year, saw_year);
}
std::ostream& operator<<(std::ostream& out,
const date_interval_t::duration_t& duration)
{
if (duration.quantum == date_interval_t::duration_t::DAYS)
out << duration.length << " day(s)";
else if (duration.quantum == date_interval_t::duration_t::WEEKS)
out << duration.length << " week(s)";
else if (duration.quantum == date_interval_t::duration_t::MONTHS)
out << duration.length << " month(s)";
else {
assert(duration.quantum == date_interval_t::duration_t::YEARS);
out << duration.length << " year(s)";
}
return out;
}
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 (end && *end_of_duration > *end) {
end_of_duration = end;
DEBUG("times.interval",
"stabilize: end_of_duration reset to end: " << *end_of_duration);
}
if (! skip_duration) {
skip_duration = duration;
DEBUG("times.interval",
"stabilize: skip_duration set to: " << *skip_duration);
}
if (start && ! next) {
next = skip_duration->add(*start);
DEBUG("times.interval",
"stabilize: next set to: " << *next);
}
}
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;
optional<date_t> initial_end = end;
#if defined(DEBUG_ON)
if (initial_start)
DEBUG("times.interval",
"stabilize: initial_start = " << *initial_start);
if (initial_end)
DEBUG("times.interval",
"stabilize: initial_end = " << *initial_end);
#endif
date_t when = start ? *start : *date;
if (duration->quantum == duration_t::MONTHS ||
duration->quantum == duration_t::YEARS) {
DEBUG("times.interval", "stabilize: monthly or yearly duration");
start = date_t(when.year(), gregorian::Jan, 1);
} else {
DEBUG("times.interval", "stabilize: daily or weekly duration");
start = date_t(when - gregorian::days(400));
if (duration->quantum == duration_t::WEEKS) {
// Move it to a Sunday
while (start->day_of_week() != start_of_week)
*start += gregorian::days(1);
}
}
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: final start date = " << *start);
if (initial_start && (! start || *start < *initial_start)) {
resolve_end();
start = initial_start;
DEBUG("times.interval", "stabilize: start reset to initial start");
}
if (initial_end && (! end || *end > *initial_end)) {
end = initial_end;
DEBUG("times.interval", "stabilize: end reset to initial end");
}
}
aligned = true;
}
// If there is no duration, then if we've reached here the date falls
// between begin and end.
if (! duration) {
DEBUG("times.interval", "stabilize: there was no duration given");
if (! start && ! end)
throw_(date_error,
_("Invalid date interval: neither start, nor end, nor duration"));
} else {
resolve_end();
}
}
bool date_interval_t::find_period(const date_t& date)
{
stabilize(date);
if (end && date > *end) {
DEBUG("times.interval",
"false: date [" << date << "] > end [" << *end << "]");
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);
while (date >= scan && (! end || scan < *end)) {
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);
return true;
}
scan = skip_duration->add(scan);
end_of_scan = duration->add(scan);
}
return false;
}
date_interval_t& date_interval_t::operator++()
{
if (! start)
throw_(date_error, _("Cannot increment an unstarted date interval"));
stabilize();
if (! skip_duration || ! duration)
throw_(date_error,
_("Cannot increment a date interval without a duration"));
assert(next);
if (end && *next >= *end) {
start = none;
} else {
start = *next;
end_of_duration = duration->add(*start);
}
next = none;
resolve_end();
return *this;
}
namespace {
void parse_inclusion_specifier(const string& word,
date_t * begin,
date_t * end)
{
bool saw_year = true;
date_t when = parse_date_mask(word.c_str(), none, saw_year);
if (when.is_not_a_date())
throw_(date_error, _("Could not parse date mask: %1") << word);
if (begin) {
*begin = when;
if (end) {
if (saw_year)
*end = *begin + gregorian::years(1);
else
*end = *begin + gregorian::months(1);
}
}
else if (end) {
*end = when;
}
}
inline void read_lower_word(std::istream& in, string& word) {
in >> word;
for (string::size_type i = 0, l = word.length(); i < l; i++)
word[i] = static_cast<char>(std::tolower(word[i]));
}
void parse_date_words(std::istream& in,
string& word,
date_interval_t& interval,
bool look_for_start = true,
bool look_for_end = true)
{
string type;
if (word == _("this") || word == _("last") || word == _("next")) {
type = word;
if (! in.eof())
read_lower_word(in, word);
else
word = _("month");
} else {
type = _("this");
}
date_t start = CURRENT_DATE();
date_t end;
bool parse_specifier = false;
optional<date_interval_t::duration_t> duration;
assert(look_for_start || look_for_end);
if (word == _("year")) {
duration = date_interval_t::duration_t(date_interval_t::duration_t::YEARS, 1);
start = gregorian::date(start.year(), 1, 1);
}
else if (word == _("month")) {
duration = date_interval_t::duration_t(date_interval_t::duration_t::MONTHS, 1);
start = gregorian::date(start.year(), start.month(), 1);
}
else if (word == _("today") || word == _("day")) {
duration = date_interval_t::duration_t(date_interval_t::duration_t::DAYS, 1);
}
else {
parse_specifier = true;
}
if (parse_specifier)
parse_inclusion_specifier(word, &start, &end);
else
end = duration->add(start);
if (type == _("last") && duration) {
start = duration->subtract(start);
end = duration->subtract(end);
}
else if (type == _("next") && duration) {
start = duration->add(start);
end = duration->add(end);
}
if (look_for_start && is_valid(start)) interval.start = start;
if (look_for_end && is_valid(end)) interval.end = end;
}
}
void date_interval_t::parse(std::istream& in)
{
string word;
optional<date_time::months_of_year> mon;
optional<date_time::weekdays> wday;
optional<date_t::year_type> year;
while (! in.eof()) {
read_lower_word(in, word);
if (word == _("every")) {
read_lower_word(in, word);
if (std::isdigit(word[0])) {
int quantity = lexical_cast<int>(word);
read_lower_word(in, word);
if (word == _("days"))
duration = duration_t(duration_t::DAYS, quantity);
else if (word == _("weeks"))
duration = duration_t(duration_t::WEEKS, quantity);
else if (word == _("months"))
duration = duration_t(duration_t::MONTHS, quantity);
else if (word == _("quarters"))
duration = duration_t(duration_t::MONTHS, 3 * quantity);
else if (word == _("years"))
duration = duration_t(duration_t::YEARS, quantity);
}
else if (word == _("day"))
duration = duration_t(duration_t::DAYS, 1);
else if (word == _("week"))
duration = duration_t(duration_t::WEEKS, 1);
else if (word == _("month"))
duration = duration_t(duration_t::MONTHS, 1);
else if (word == _("quarter"))
duration = duration_t(duration_t::MONTHS, 3);
else if (word == _("year"))
duration = duration_t(duration_t::YEARS, 1);
}
else if (word == _("daily"))
duration = duration_t(duration_t::DAYS, 1);
else if (word == _("weekly"))
duration = duration_t(duration_t::WEEKS, 1);
else if (word == _("biweekly"))
duration = duration_t(duration_t::WEEKS, 2);
else if (word == _("monthly"))
duration = duration_t(duration_t::MONTHS, 1);
else if (word == _("bimonthly"))
duration = duration_t(duration_t::MONTHS, 2);
else if (word == _("quarterly"))
duration = duration_t(duration_t::MONTHS, 3);
else if (word == _("yearly"))
duration = duration_t(duration_t::YEARS, 1);
else if (word == _("this") || word == _("last") || word == _("next") ||
word == _("today")) {
parse_date_words(in, word, *this);
}
else if (word == _("in")) {
read_lower_word(in, word);
parse_date_words(in, word, *this);
}
else if (word == _("from") || word == _("since")) {
read_lower_word(in, word);
parse_date_words(in, word, *this, true, false);
}
else if (word == _("to") || word == _("until")) {
read_lower_word(in, word);
parse_date_words(in, word, *this, false, true);
}
else if (optional<date_time::months_of_year>
m = string_to_month_of_year(word)) {
mon = m;
}
else if (optional<date_time::weekdays>
d = string_to_day_of_week(word)) {
wday = d;
}
else if (all(word, is_digit())) {
year = lexical_cast<unsigned short>(word);
}
else {
// otherwise, it should be an explicit date
date_t b, e;
parse_inclusion_specifier(word, &b, &e);
start = b;
end = e;
}
}
if (year || mon || wday) {
if (! start)
start = CURRENT_DATE();
if (wday) {
while (start->day_of_week() != *wday)
*start = duration_t(duration_t::DAYS, 1).subtract(*start);
if (! end)
end = duration_t(duration_t::DAYS, 1).add(*start);
} else {
bool overwrite_end = false;
if (year) {
start = date_t(*year, 1, 1);
if (! end) {
end = duration_t(duration_t::YEARS, 1).add(*start);
overwrite_end = true;
}
}
if (mon) {
start = date_t(start->year(), *mon, 1);
if (! end || overwrite_end)
end = duration_t(duration_t::MONTHS, 1).add(*start);
}
}
}
}
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(0);
return "";
}
}
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(0);
return "";
}
}
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)
{
input_date_io.reset(new date_io_t(format, true));
}
void times_initialize()
{
if (! is_initialized) {
input_datetime_io.reset(new datetime_io_t("%Y/%m/%d %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();
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;
}
}
} // namespace ledger