diff options
Diffstat (limited to 'src/times.cc')
-rw-r--r-- | src/times.cc | 1587 |
1 files changed, 1587 insertions, 0 deletions
diff --git a/src/times.cc b/src/times.cc new file mode 100644 index 00000000..d4317509 --- /dev/null +++ b/src/times.cc @@ -0,0 +1,1587 @@ +/* + * 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: + 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 * 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_year year, + date_traits_t * traits = NULL) + { + 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 (traits) + *traits = io.traits; + + if (! io.traits.has_year) { + when = date_t(year ? *year : 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, optional_year year, + date_traits_t * traits = NULL) + { + if (input_date_io.get()) { + date_t when = parse_date_mask_routine(date_str, *input_date_io.get(), + year, 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(), + year, 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, optional_year) +{ + 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_year current_year) +{ + return parse_date_mask(str, current_year); +} + +date_t date_specifier_t::begin(const optional_year& current_year) const +{ + assert(year || current_year); + + year_type the_year = year ? *year : static_cast<year_type>(*current_year); + month_type the_month = month ? *month : date_t::month_type(1); + day_type the_day = day ? *day : date_t::day_type(1); + + if (day) + assert(! wday); + else if (wday) + assert(! day); + + // 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 optional_year& current_year) const +{ + if (day || wday) + return begin(current_year) + gregorian::days(1); + else if (month) + return begin(current_year) + gregorian::months(1); + else if (year) + return begin(current_year) + 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_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_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>"; + default: + assert(false); + return empty_string; + } + + 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_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; + default: + assert(false); + 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& lexer) + : begin(lexer.begin), end(lexer.end), + token_cache(lexer.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) +{ + 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: + specifier.day = + date_specifier_t::day_type(boost::get<unsigned short>(*tok.value)); + 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; + + 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) { +#if 0 + case lexer_t::token_t::TOK_INT: + // jww (2009-11-18): NYI + assert(! "Need to allow for expressions like \"4 months ago\""); + tok.unexpected(); + break; +#endif + + 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: + // jww (2009-11-18): Allow things like "last 5 weeks" + assert(! "Need to allow for expressions like \"last 5 weeks\""); + tok.unexpected(); + 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 temp = + date_duration_t::find_nearest(today, date_duration_t::QUARTERS); + temp += gregorian::months(3 * adjust); + inclusion_specifier = + date_specifier_t(static_cast<date_specifier_t::year_type>(temp.year()), + temp.month()); +#if 0 + period.duration = date_duration_t(date_duration_t::QUARTERS, 1); +#endif + break; + } + + 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; + } + + case lexer_t::token_t::TOK_WEEK: { + date_t temp = + date_duration_t::find_nearest(today, date_duration_t::WEEKS); + temp += gregorian::days(7 * adjust); + inclusion_specifier = date_specifier_t(today); +#if 0 + period.duration = date_duration_t(date_duration_t::WEEKS, 1); +#endif + break; + } + + case lexer_t::token_t::TOK_DAY: { + date_t temp(today); + temp += gregorian::days(adjust); + inclusion_specifier = date_specifier_t(temp); + break; + } + + default: + tok.unexpected(); + 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 == 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; + default: + assert(false); + 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(date->year()); + optional<date_t> initial_finish = finish ? finish : end(date->year()); + +#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) { + if (date) { + start = range->begin(date->year()); + finish = range->end(date->year()); + } else { + 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) +{ + 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); + + 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); + + return true; + } + + scan = 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 (! 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, optional_year current_year) +{ + 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; + + stabilize(begin(current_year)); + + 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)) { + try { + string::const_iterator i = begin; + for (i = begin; i != end && ! std::isspace(*i); i++) {} + assert(i != begin); + + string possible_date(start, i); + date_traits_t traits; + + date_t when = parse_date_mask(possible_date.c_str(), none, &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 (...) {} + } + + 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 == _("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) +{ + 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; + } +} + +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 |