diff options
author | John Wiegley <johnw@newartisans.com> | 2009-11-19 03:37:16 -0500 |
---|---|---|
committer | John Wiegley <johnw@newartisans.com> | 2009-11-19 03:37:16 -0500 |
commit | cc9110a43a1e2d006de8d24a84bbfb2c6918cf33 (patch) | |
tree | 4230ae03ebea8e2cbc0b6e1de821730e4819af47 /src/times.cc | |
parent | 6b557f810e1c6eb2dcd8a24eeeacfcf9cc8210e3 (diff) | |
parent | 63fee4c83775f79364199ea547dbc7e068b0abc8 (diff) | |
download | fork-ledger-cc9110a43a1e2d006de8d24a84bbfb2c6918cf33.tar.gz fork-ledger-cc9110a43a1e2d006de8d24a84bbfb2c6918cf33.tar.bz2 fork-ledger-cc9110a43a1e2d006de8d24a84bbfb2c6918cf33.zip |
Merge branch 'next'
Diffstat (limited to 'src/times.cc')
-rw-r--r-- | src/times.cc | 1209 |
1 files changed, 954 insertions, 255 deletions
diff --git a/src/times.cc b/src/times.cc index 6afbab0a..e3ccaff8 100644 --- a/src/times.cc +++ b/src/times.cc @@ -55,13 +55,15 @@ namespace { #endif // USE_BOOST_FACETS public: - bool has_year; - bool has_day; + date_traits_t traits; bool input; temporal_io_t(const char * _fmt_str, bool _input) - : fmt_str(_fmt_str), has_year(icontains(fmt_str, "%y")), - has_day(icontains(fmt_str, "%d")), input(_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); @@ -75,9 +77,10 @@ namespace { void set_format(const char * fmt) { fmt_str = fmt; - has_year = icontains(fmt_str, "%y"); - has_day = icontains(fmt_str, "%d"); - + 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); @@ -191,8 +194,8 @@ namespace { 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, bool& saw_day) + optional_year year, + date_traits_t * traits = NULL) { date_t when; @@ -214,38 +217,33 @@ namespace { DEBUG("times.parse", "Parsed date string: " << date_str); DEBUG("times.parse", "Parsed result is: " << when); - if (! io.has_year) { - saw_year = false; + 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); } - else { - saw_year = true; - } - - saw_day = io.has_day; } return when; } - date_t parse_date_mask(const char * date_str, - optional<date_t::year_type> year, - bool& saw_year, bool& saw_day) + 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, saw_year, saw_day); + 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, saw_year, saw_day); + year, traits); if (! when.is_not_a_date()) return when; } @@ -306,7 +304,7 @@ string_to_month_of_year(const std::string& str) return none; } -datetime_t parse_datetime(const char * str, optional<date_t::year_type>) +datetime_t parse_datetime(const char * str, optional_year) { datetime_t when = input_datetime_io->parse(str); if (when.is_not_a_date_time()) @@ -314,29 +312,666 @@ datetime_t parse_datetime(const char * str, optional<date_t::year_type>) return when; } -date_t parse_date(const char * str, optional<date_t::year_type> current_year) +date_t parse_date(const char * str, optional_year current_year) { - bool saw_year; - bool saw_day; - return parse_date_mask(str, current_year, saw_year, saw_day); + 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_interval_t::duration_t& duration) + const date_duration_t& duration) { - if (duration.quantum == date_interval_t::duration_t::DAYS) + if (duration.quantum == date_duration_t::DAYS) out << duration.length << " day(s)"; - else if (duration.quantum == date_interval_t::duration_t::WEEKS) + else if (duration.quantum == date_duration_t::WEEKS) out << duration.length << " week(s)"; - else if (duration.quantum == date_interval_t::duration_t::MONTHS) + 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_interval_t::duration_t::YEARS); + 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) { @@ -345,23 +980,50 @@ void date_interval_t::resolve_end() "stabilize: end_of_duration = " << *end_of_duration); } - if (end && *end_of_duration > *end) { - end_of_duration = end; + if (finish && *end_of_duration > *finish) { + end_of_duration = finish; 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 = end_of_duration; + DEBUG("times.interval", "stabilize: next set to: " << *next); } +} - if (start && ! next) { - next = skip_duration->add(*start); - 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) @@ -384,35 +1046,30 @@ void date_interval_t::stabilize(const optional<date_t>& date) // 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; + 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_end) + "stabilize: initial_start = " << *initial_start); + if (initial_finish) DEBUG("times.interval", - "stabilize: initial_end = " << *initial_end); + "stabilize: initial_finish = " << *initial_finish); #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); + 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_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); - } + start = date_duration_t::find_nearest(when - gregorian::days(400), + duration->quantum); } DEBUG("times.interval", @@ -431,30 +1088,47 @@ void date_interval_t::stabilize(const optional<date_t>& date) } } - DEBUG("times.interval", "stabilize: final start date = " << *start); + 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_end && (! end || *end > *initial_end)) { - end = initial_end; - DEBUG("times.interval", "stabilize: end reset to initial end"); + 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 begin and end. + // between start and finish. if (! duration) { DEBUG("times.interval", "stabilize: there was no duration given"); - if (! start && ! end) + if (! start && ! finish) throw_(date_error, - _("Invalid date interval: neither start, nor end, nor duration")); + _("Invalid date interval: neither start, nor finish, nor duration")); } else { resolve_end(); } @@ -464,9 +1138,9 @@ bool date_interval_t::find_period(const date_t& date) { stabilize(date); - if (end && date > *end) { + if (finish && date > *finish) { DEBUG("times.interval", - "false: date [" << date << "] > end [" << *end << "]"); + "false: date [" << date << "] > finish [" << *finish << "]"); return false; } @@ -503,7 +1177,7 @@ bool date_interval_t::find_period(const date_t& date) DEBUG("times.interval", "scan = " << scan); DEBUG("times.interval", "end_of_scan = " << end_of_scan); - while (date >= scan && (! end || scan < *end)) { + while (date >= scan && (! finish || scan < *finish)) { if (date < end_of_scan) { start = scan; end_of_duration = end_of_scan; @@ -515,7 +1189,7 @@ bool date_interval_t::find_period(const date_t& date) return true; } - scan = skip_duration->add(scan); + scan = duration->add(scan); end_of_scan = duration->add(scan); } @@ -529,20 +1203,18 @@ date_interval_t& date_interval_t::operator++() stabilize(); - if (! skip_duration || ! duration) + if (! duration) throw_(date_error, _("Cannot increment a date interval without a duration")); assert(next); - if (end && *next >= *end) { + if (finish && *next >= *finish) { start = none; } else { start = *next; - end_of_duration = duration->add(*start); } - next = none; resolve_end(); @@ -550,213 +1222,224 @@ date_interval_t& date_interval_t::operator++() return *this; } -namespace { - void parse_inclusion_specifier(const string& word, - date_t * begin, - date_t * end) - { - bool saw_year = true; - bool saw_day = true; - date_t when = parse_date_mask(word.c_str(), none, saw_year, saw_day); +void date_interval_t::dump(std::ostream& out, optional_year current_year) +{ + out << _("--- Before stabilization ---") << std::endl; - if (when.is_not_a_date()) - throw_(date_error, _("Could not parse date mask: %1") << word); + 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 (begin) { - *begin = when; + if (duration) + out << _("duration: ") << duration->to_string() << std::endl; - if (end) { - if (saw_day) - *end = *begin + gregorian::days(1); - else - *end = *begin + gregorian::months(1); - } - } - else if (end) { - *end = when; - } - } + stabilize(begin(current_year)); - 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])); - } + out << std::endl + << _("--- After stabilization ---") << std::endl; - 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 (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 (word == _("this") || word == _("last") || word == _("next")) { - type = word; - if (! in.eof()) - read_lower_word(in, word); - else - word = _("month"); - } else { - type = _("this"); - } + if (duration) + out << _("duration: ") << duration->to_string() << std::endl; - date_t start = CURRENT_DATE(); - date_t end; - bool parse_specifier = false; + out << std::endl + << _("--- Sample dates in range (max. 20) ---") << std::endl; - optional<date_interval_t::duration_t> duration; + date_t last_date; - assert(look_for_start || look_for_end); + for (int i = 0; i < 20 && *this; ++i, ++*this) { + out << std::right; + out.width(2); - 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 (! last_date.is_not_a_date() && last_date == *start) + break; - if (parse_specifier) - parse_inclusion_specifier(word, &start, &end); - else - end = duration->add(start); + out << (i + 1) << ": " << format_date(*start); + if (duration) + out << " -- " << format_date(*inclusive_end()); + out << std::endl; - 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 (! duration) + break; - if (look_for_start && is_valid(start)) interval.start = start; - if (look_for_end && is_valid(end)) interval.end = end; + last_date = *start; } } -void date_interval_t::parse(std::istream& in) +date_parser_t::lexer_t::token_t date_parser_t::lexer_t::next_token() { - 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 (token_cache.kind != token_t::UNKNOWN) { + token_t tok = token_cache; + token_cache = token_t(); + return tok; } - if (year || mon || wday) { - if (! start) - start = CURRENT_DATE(); + while (begin != end && std::isspace(*begin)) + begin++; - if (wday) { - while (start->day_of_week() != *wday) - *start -= gregorian::days(1); + if (begin == end) + return token_t(token_t::END_REACHED); - if (! end) - end = *start + gregorian::days(1); - } else { - bool overwrite_end = false; + 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; + } - if (year) { - start = date_t(*year, 1, 1); - if (! end) { - end = *start + gregorian::years(1); - overwrite_end = true; - } + 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 (...) {} + } - if (mon) { - start = date_t(start->year(), *mon, 1); - if (! end || overwrite_end) - end = *start + gregorian::months(1); + string term; + bool alnum = std::isalnum(*begin); + for (start = begin; (begin != end && ! std::isspace(*begin) && + alnum == 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); } } @@ -882,4 +1565,20 @@ void times_shutdown() 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 |