diff options
author | John Wiegley <johnw@newartisans.com> | 2009-03-16 04:02:56 -0400 |
---|---|---|
committer | John Wiegley <johnw@newartisans.com> | 2009-03-16 04:02:56 -0400 |
commit | d0ea10f9a709de8f67b4c4eb1e763ab32309957f (patch) | |
tree | 4e968c574a1c84bbf23493b57bfe61e4ac852cf7 | |
parent | 63080a727540e5168d6eb230e86d06ad390dc6c5 (diff) | |
parent | f1523b5464924bf9d9987fce0cb3dbe4acda5a4b (diff) | |
download | fork-ledger-d0ea10f9a709de8f67b4c4eb1e763ab32309957f.tar.gz fork-ledger-d0ea10f9a709de8f67b4c4eb1e763ab32309957f.tar.bz2 fork-ledger-d0ea10f9a709de8f67b4c4eb1e763ab32309957f.zip |
Rewrote the interval_t class
The purpose of this rewrite is to greatly simplify the code that walks
through time periods, toward opening up the possibility in future of
allowing exclusions and logically combined periods, such as "weekly
except weekends". The new code is much simpler to use, as well as
simpler internally, and yet is more robust at the same time.
-rw-r--r-- | src/commodity.cc | 2 | ||||
-rw-r--r-- | src/filters.cc | 105 | ||||
-rw-r--r-- | src/filters.h | 48 | ||||
-rw-r--r-- | src/precmd.cc | 53 | ||||
-rw-r--r-- | src/report.h | 12 | ||||
-rw-r--r-- | src/textual.cc | 2 | ||||
-rw-r--r-- | src/times.cc | 440 | ||||
-rw-r--r-- | src/times.h | 115 | ||||
-rw-r--r-- | src/token.cc | 7 | ||||
-rw-r--r-- | src/xact.h | 6 |
10 files changed, 523 insertions, 267 deletions
diff --git a/src/commodity.cc b/src/commodity.cc index 22c92862..3e4d1160 100644 --- a/src/commodity.cc +++ b/src/commodity.cc @@ -936,7 +936,7 @@ namespace { details.print(name, comm.parent().keep_base); DEBUG("amounts.commodities", "make_qualified_name for " - << comm.qualified_symbol << std::endl << details); + << *comm.qualified_symbol << std::endl << details); DEBUG("amounts.commodities", "qualified_name is " << name.str()); return name.str(); diff --git a/src/filters.cc b/src/filters.cc index d0e345ae..14b18db1 100644 --- a/src/filters.cc +++ b/src/filters.cc @@ -483,41 +483,41 @@ void changed_value_posts::operator()(post_t& post) last_post = &post; } -void subtotal_posts::report_subtotal(const char * spec_fmt, - const date_t& start, - const date_t& finish) +void subtotal_posts::report_subtotal(const char * spec_fmt, + const optional<date_interval_t>& interval) { if (component_posts.empty()) return; - date_t range_start = start; - date_t range_finish = finish; + optional<date_t> range_start = interval ? interval->start : none; + optional<date_t> range_finish = interval ? interval->inclusive_end() : none; + foreach (post_t * post, component_posts) { date_t date = post->date(); - if (! is_valid(range_start) || date < range_start) + if (! range_start || date < *range_start) range_start = date; - if (! is_valid(range_finish) || date > range_finish) + if (! range_finish || date > *range_finish) range_finish = date; } component_posts.clear(); std::ostringstream out_date; if (spec_fmt) { - out_date << format_date(range_finish, string(spec_fmt)); + out_date << format_date(*range_finish, string(spec_fmt)); } else if (date_format) { string fmt = "- "; fmt += *date_format; - out_date << format_date(range_finish, string(fmt)); + out_date << format_date(*range_finish, string(fmt)); } else { - out_date << format_date(range_finish, std::string("- ") + output_date_format); + out_date << format_date(*range_finish, std::string("- ") + output_date_format); } xact_temps.push_back(xact_t()); xact_t& xact = xact_temps.back(); xact.payee = out_date.str(); - xact._date = range_start; + xact._date = *range_start; foreach (values_map::value_type& pair, values) handle_value(pair.second.value, pair.second.account, &xact, post_temps, @@ -556,13 +556,13 @@ void subtotal_posts::operator()(post_t& post) post.reported_account()->xdata().add_flags(ACCOUNT_EXT_HAS_UNB_VIRTUALS); } -void interval_posts::report_subtotal(const date_t& finish) +void interval_posts::report_subtotal(const date_interval_t& interval) { if (last_post && interval) { if (exact_periods) subtotal_posts::report_subtotal(); else - subtotal_posts::report_subtotal(NULL, interval.begin, finish); + subtotal_posts::report_subtotal(NULL, interval); } last_post = NULL; @@ -572,35 +572,22 @@ void interval_posts::operator()(post_t& post) { date_t date = post.date(); - if ((is_valid(interval.begin) && date < interval.begin) || - (is_valid(interval.end) && date >= interval.end)) + if (! interval.find_period(post.date())) return; - if (interval) { - if (! is_valid(interval.begin)) - interval.set_start(date); - start = interval.begin; - - date_t quant = interval.increment(interval.begin); - if (date >= quant) { - if (last_post) - report_subtotal(quant - gregorian::days(1)); - - date_t temp; - while (date >= (temp = interval.increment(quant))) { - if (quant == temp) - break; - interval.begin = quant; - quant = temp; + if (interval.duration) { + if (last_interval && interval != last_interval) { + report_subtotal(last_interval); - if (generate_empty_posts) { + if (generate_empty_posts) { + for (++last_interval; interval != last_interval; ++last_interval) { // Generate a null posting, so the intervening periods can be // seen when -E is used, or if the calculated amount ends up being // non-zero xact_temps.push_back(xact_t()); xact_t& null_xact = xact_temps.back(); null_xact.add_flags(ITEM_TEMP); - null_xact._date = quant - gregorian::days(1); + null_xact._date = last_interval.inclusive_end(); post_temps.push_back(post_t(&empty_account)); post_t& null_post = post_temps.back(); @@ -611,10 +598,14 @@ void interval_posts::operator()(post_t& post) last_post = &null_post; subtotal_posts::operator()(null_post); - report_subtotal(quant - gregorian::days(1)); + report_subtotal(last_interval); } + assert(interval == last_interval); + } else { + last_interval = interval; } - start = interval.begin = quant; + } else { + last_interval = interval; } subtotal_posts::operator()(post); } else { @@ -745,7 +736,7 @@ void generate_posts::add_period_xacts(period_xacts_list& period_xacts) add_post(xact->period, *post); } -void generate_posts::add_post(const interval_t& period, post_t& post) +void generate_posts::add_post(const date_interval_t& period, post_t& post) { pending_posts.push_back(pending_posts_pair(period, &post)); } @@ -759,14 +750,16 @@ void budget_posts::report_budget_items(const date_t& date) do { reported = false; foreach (pending_posts_list::value_type& pair, pending_posts) { - date_t& begin = pair.first.begin; - if (! is_valid(begin)) { - pair.first.set_start(date); - begin = pair.first.begin; + optional<date_t> begin = pair.first.start; + if (! begin) { + if (! pair.first.find_period(date)) + throw_(std::runtime_error, _()"Something odd has happened"); + begin = pair.first.start; } + assert(begin); - if (begin < date && - (! is_valid(pair.first.end) || begin < pair.first.end)) { + if (*begin < date && + (! pair.first.end || *begin < *pair.first.end)) { post_t& post = *pair.second; DEBUG("ledger.walk.budget", "Reporting budget for " @@ -784,7 +777,8 @@ void budget_posts::report_budget_items(const date_t& date) temp.amount.in_place_negate(); xact.add_post(&temp); - begin = pair.first.increment(begin); + ++pair.first; + begin = *pair.first.start; item_handler<post_t>::operator()(temp); @@ -823,17 +817,18 @@ void budget_posts::operator()(post_t& post) } } -void forecast_posts::add_post(const interval_t& period, post_t& post) +void forecast_posts::add_post(const date_interval_t& period, post_t& post) { generate_posts::add_post(period, post); - interval_t& i = pending_posts.back().first; - if (! is_valid(i.begin)) { - i.set_start(CURRENT_DATE()); - i.begin = i.increment(i.begin); + date_interval_t& i = pending_posts.back().first; + if (! i.start) { + if (! i.find_period(CURRENT_DATE())) + throw_(std::runtime_error, _("Something odd has happened")); + ++i; } else { - while (i.begin < CURRENT_DATE()) - i.begin = i.increment(i.begin); + while (*i.start < CURRENT_DATE()) + ++i; } } @@ -847,12 +842,12 @@ void forecast_posts::flush() for (pending_posts_list::iterator i = ++pending_posts.begin(); i != pending_posts.end(); i++) - if ((*i).first.begin < (*least).first.begin) + if (*(*i).first.start < *(*least).first.start) least = i; - date_t& begin = (*least).first.begin; + date_t& begin = *(*least).first.start; - if (is_valid((*least).first.end) && begin >= (*least).first.end) { + if ((*least).first.end && begin >= *(*least).first.end) { pending_posts.erase(least); passed.remove((*least).second); continue; @@ -871,7 +866,9 @@ void forecast_posts::flush() temp.add_flags(ITEM_TEMP); xact.add_post(&temp); - date_t next = (*least).first.increment(begin); + date_t next = *(*least).first.next; + ++(*least).first; + if (next < begin || (is_valid(last) && (next - last).days() > 365 * 5)) break; begin = next; diff --git a/src/filters.h b/src/filters.h index d3968c51..14b97c23 100644 --- a/src/filters.h +++ b/src/filters.h @@ -539,9 +539,8 @@ public: clear_xacts_posts(xact_temps); } - void report_subtotal(const char * spec_fmt = NULL, - const date_t& start = date_t(), - const date_t& finish = date_t()); + void report_subtotal(const char * spec_fmt = NULL, + const optional<date_interval_t>& interval = none); virtual void flush() { if (values.size() > 0) @@ -558,22 +557,22 @@ public: */ class interval_posts : public subtotal_posts { - interval_t interval; - post_t * last_post; - account_t empty_account; - bool exact_periods; - bool generate_empty_posts; - date_t start; + date_interval_t interval; + date_interval_t last_interval; + post_t * last_post; + account_t empty_account; + bool exact_periods; + bool generate_empty_posts; interval_posts(); public: - interval_posts(post_handler_ptr _handler, - expr_t& amount_expr, - const interval_t& _interval, - bool _exact_periods = false, - bool _generate_empty_posts = false) + interval_posts(post_handler_ptr _handler, + expr_t& amount_expr, + const date_interval_t& _interval, + bool _exact_periods = false, + bool _generate_empty_posts = false) : subtotal_posts(_handler, amount_expr), interval(_interval), last_post(NULL), empty_account(NULL, _("<None>")), exact_periods(_exact_periods), @@ -585,19 +584,20 @@ public: TRACE_DTOR(interval_posts); } - void report_subtotal(const date_t& finish); + void report_subtotal(const date_interval_t& interval); virtual void flush() { - if (last_post && interval) - report_subtotal(interval.increment(interval.begin) - gregorian::days(1)); - subtotal_posts::flush(); + if (last_post && interval.duration) { + if (interval.is_valid()) + report_subtotal(interval); + subtotal_posts::flush(); + } } virtual void operator()(post_t& post); }; class posts_as_equity : public subtotal_posts { - interval_t interval; post_t * last_post; account_t equity_account; account_t * balance_account; @@ -726,11 +726,11 @@ class generate_posts : public item_handler<post_t> generate_posts(); protected: - typedef std::pair<interval_t, post_t *> pending_posts_pair; - typedef std::list<pending_posts_pair> pending_posts_list; + typedef std::pair<date_interval_t, post_t *> pending_posts_pair; + typedef std::list<pending_posts_pair> pending_posts_list; pending_posts_list pending_posts; - std::list<xact_t> xact_temps; + std::list<xact_t> xact_temps; std::list<post_t> post_temps; public: @@ -746,7 +746,7 @@ public: void add_period_xacts(period_xacts_list& period_xacts); - virtual void add_post(const interval_t& period, post_t& post); + virtual void add_post(const date_interval_t& period, post_t& post); }; /** @@ -801,7 +801,7 @@ class forecast_posts : public generate_posts TRACE_DTOR(forecast_posts); } - virtual void add_post(const interval_t& period, post_t& post); + virtual void add_post(const date_interval_t& period, post_t& post); virtual void flush(); }; diff --git a/src/precmd.cc b/src/precmd.cc index ef04cb79..1160cc64 100644 --- a/src/precmd.cc +++ b/src/precmd.cc @@ -160,25 +160,54 @@ value_t period_command(call_scope_t& args) report_t& report(find_scope<report_t>(args)); std::ostream& out(report.output_stream); - interval_t interval(arg); + date_interval_t interval(arg); + + out << _("global details => ") << std::endl << std::endl; + + if (interval.start) + out << _(" start: ") << format_date(*interval.start) << std::endl; + else + out << _(" start: TODAY: ") << format_date(CURRENT_DATE()) << std::endl; + if (interval.end) + out << _(" end: ") << format_date(*interval.end) << std::endl; + + if (interval.skip_duration) + out << _(" skip: ") << *interval.skip_duration << std::endl; + if (interval.factor) + out << _(" factor: ") << interval.factor << std::endl; + if (interval.duration) + out << _("duration: ") << *interval.duration << std::endl; + + if (interval.find_period(interval.start ? + *interval.start : CURRENT_DATE())) { + out << std::endl + << _("after finding first period => ") << std::endl + << std::endl; + + if (interval.start) + out << _(" start: ") << format_date(*interval.start) << std::endl; + if (interval.end) + out << _(" end: ") << format_date(*interval.end) << std::endl; + + if (interval.skip_duration) + out << _(" skip: ") << *interval.skip_duration << std::endl; + if (interval.factor) + out << _(" factor: ") << interval.factor << std::endl; + if (interval.duration) + out << _("duration: ") << *interval.duration << std::endl; - if (! is_valid(interval.begin)) { - out << _("Time period has no beginning.") << std::endl; - } else { - out << _("begin: ") << format_date(interval.begin) << std::endl; - out << _(" end: ") << format_date(interval.end) << std::endl; out << std::endl; - date_t date = interval.first(); - - for (int i = 0; i < 20; i++) { + for (int i = 0; i < 20 && interval; i++, ++interval) { out << std::right; out.width(2); - out << i << "): " << format_date(date) << std::endl; + out << i << "): " << format_date(*interval.start); + if (interval.end_of_duration) + out << " -- " << format_date(*interval.inclusive_end()); + out << std::endl; - date = interval.increment(date); - if (is_valid(interval.end) && date >= interval.end) + if (! interval.skip_duration) break; } } diff --git a/src/report.h b/src/report.h index 55c37b22..5eb2a706 100644 --- a/src/report.h +++ b/src/report.h @@ -240,14 +240,14 @@ public: }); OPTION_(report_t, begin_, DO_(args) { // -b - interval_t interval(args[0].to_string()); - if (! is_valid(interval.begin)) + date_interval_t interval(args[0].to_string()); + if (! interval.start) throw_(std::invalid_argument, _("Could not determine beginning of period '%1'") << args[0].to_string()); string predicate = - "date>=[" + to_iso_extended_string(interval.begin) + "]"; + "date>=[" + to_iso_extended_string(*interval.start) + "]"; parent->HANDLER(limit_).on(predicate); }); @@ -355,14 +355,14 @@ public: OPTION(report_t, empty); // -E OPTION_(report_t, end_, DO_(args) { // -e - interval_t interval(args[0].to_string()); - if (! is_valid(interval.begin)) + date_interval_t interval(args[0].to_string()); + if (! interval.start) throw_(std::invalid_argument, _("Could not determine end of period '%1'") << args[0].to_string()); string predicate = - "date<[" + to_iso_extended_string(interval.begin) + "]"; + "date<[" + to_iso_extended_string(*interval.start) + "]"; parent->HANDLER(limit_).on(predicate); #if 0 terminus = interval.begin; diff --git a/src/textual.cc b/src/textual.cc index 8f7388a2..0c92f7bb 100644 --- a/src/textual.cc +++ b/src/textual.cc @@ -592,8 +592,6 @@ void instance_t::period_xact_directive(char * line) try { std::auto_ptr<period_xact_t> pe(new period_xact_t(skip_ws(line + 1))); - if (! pe->period) - throw_(parse_error, _("Parsing time period '%1'") << line); reveal_context = false; diff --git a/src/times.cc b/src/times.cc index d83b6baa..4bf5241b 100644 --- a/src/times.cc +++ b/src/times.cc @@ -107,25 +107,25 @@ namespace { } } -int string_to_day_of_week(const std::string& str) +date_time::weekdays string_to_day_of_week(const std::string& str) { if (str == _("sun") || str == _("sunday") || str == "0") - return 0; + return gregorian::Sunday; else if (str == _("mon") || str == _("monday") || str == "1") - return 1; + return gregorian::Monday; else if (str == _("tue") || str == _("tuesday") || str == "2") - return 2; + return gregorian::Tuesday; else if (str == _("wed") || str == _("wednesday") || str == "3") - return 3; + return gregorian::Wednesday; else if (str == _("thu") || str == _("thursday") || str == "4") - return 4; + return gregorian::Thursday; else if (str == _("fri") || str == _("friday") || str == "5") - return 5; + return gregorian::Friday; else if (str == _("sat") || str == _("saturday") || str == "6") - return 6; + return gregorian::Saturday; assert(false); - return -1; + return gregorian::Sunday; } datetime_t parse_datetime(const char * str, int) @@ -145,50 +145,260 @@ date_t parse_date(const char * str, int current_year) return gregorian::date_from_tm(when); } -date_t interval_t::first(const optional<date_t>& moment) +date_t date_interval_t::add_duration(const date_t& date, + const duration_t& duration) { - if (! is_valid(begin)) { - // 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. - assert(moment); - - if (months > 0 || years > 0) { - begin = date_t(moment->year(), gregorian::Jan, 1); - } else { - begin = date_t(*moment - gregorian::days(400)); + if (duration.type() == typeid(gregorian::days)) + return date + boost::get<gregorian::days>(duration); + else if (duration.type() == typeid(gregorian::weeks)) + return date + boost::get<gregorian::weeks>(duration); + else if (duration.type() == typeid(gregorian::months)) + return date + boost::get<gregorian::months>(duration); + else + assert(duration.type() == typeid(gregorian::years)); + return date + boost::get<gregorian::years>(duration); +} + +date_t date_interval_t::subtract_duration(const date_t& date, + const duration_t& duration) +{ + if (duration.type() == typeid(gregorian::days)) + return date - boost::get<gregorian::days>(duration); + else if (duration.type() == typeid(gregorian::weeks)) + return date - boost::get<gregorian::weeks>(duration); + else if (duration.type() == typeid(gregorian::months)) + return date - boost::get<gregorian::months>(duration); + else + assert(duration.type() == typeid(gregorian::years)); + return date - boost::get<gregorian::years>(duration); +} + +std::ostream& operator<<(std::ostream& out, + const date_interval_t::duration_t& duration) +{ + if (duration.type() == typeid(gregorian::days)) + out << boost::get<gregorian::days>(duration).days() + << " day(s)"; + else if (duration.type() == typeid(gregorian::weeks)) + out << (boost::get<gregorian::weeks>(duration).days() / 7) + << " week(s)"; + else if (duration.type() == typeid(gregorian::months)) + out << boost::get<gregorian::months>(duration).number_of_months() + << " month(s)"; + else { + assert(duration.type() == typeid(gregorian::years)); + out << boost::get<gregorian::years>(duration).number_of_years() + << " year(s)"; + } + return out; +} + +void date_interval_t::resolve_end() +{ + if (start && ! end_of_duration) { + end_of_duration = add_duration(*start, *duration); + 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 = add_duration(*start, *skip_duration); + DEBUG("times.interval", + "stabilize: next set to: " << *next); + } +} - // jww (2009-02-21): Add support for starting a week on any day - if (weekly) { // move it to a Sunday - while (begin.day_of_week() != start_of_week) - begin += gregorian::days(1); +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->type() == typeid(gregorian::months) || + duration->type() == typeid(gregorian::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->type() == typeid(gregorian::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; } - date_t quant(begin); + 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 && date < *end_of_duration) { + DEBUG("times.interval", + "true: date [" << date << "] < end_of_duration [" + << *end_of_duration << "]"); + return true; + } + + // 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; - if (moment && *moment >= quant) { - date_t temp; - while (*moment >= (temp = increment(quant))) { - if (quant == temp) - break; - quant = temp; + 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 = add_duration(scan, *skip_duration); + end_of_scan = add_duration(scan, *duration); } - return quant; + + return false; } -date_t interval_t::increment(const date_t& moment) const +date_interval_t& date_interval_t::operator++() { - date_t future(moment); + 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 = add_duration(*start, *duration); + } + + next = none; - if (years) future += gregorian::years(years); - if (months) future += gregorian::months(months); - if (days) future += gregorian::days(days); + resolve_end(); - return future; + return *this; } namespace { @@ -230,9 +440,15 @@ namespace { if (begin) { *begin = gregorian::date_from_tm(when); - if (end) - *end = interval_t(saw_day ? 1 : 0, saw_mon ? 1 : 0, - saw_year ? 1 : 0).increment(*begin); + + if (end) { + if (saw_year) + *end = *begin + gregorian::years(1); + else if (saw_mon) + *end = *begin + gregorian::months(1); + else if (saw_day) + *end = *begin + gregorian::days(1); + } } else if (end) { *end = gregorian::date_from_tm(when); @@ -245,14 +461,14 @@ namespace { word[i] = static_cast<char>(std::tolower(word[i])); } - void parse_date_words(std::istream& in, string& word, - date_t * begin, date_t * end) + 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; - bool mon_spec = false; - char buf[32]; - if (word == _("this") || word == _("last") || word == _("next")) { type = word; if (! in.eof()) @@ -263,59 +479,48 @@ namespace { type = _("this"); } - if (word == _("month")) { - time_t now = to_time_t(CURRENT_TIME()); - std::strftime(buf, 31, "%B", localtime(&now)); - word = buf; - mon_spec = true; + date_t start = CURRENT_DATE(); + date_t end; + bool parse_specifier = false; + + date_interval_t::duration_t duration; + + assert(look_for_start || look_for_end); + + if (word == _("year")) { + duration = gregorian::years(1); + start = gregorian::date(start.year(), 1, 1); } - else if (word == _("year")) { - int year = CURRENT_DATE().year(); - std::sprintf(buf, "%04d", year); - word = buf; + else if (word == _("month")) { + duration = gregorian::months(1); + start = gregorian::date(start.year(), start.month(), 1); } - else if (word == _("today")) { - if (begin) - *begin = CURRENT_DATE(); - if (end) { - *end = CURRENT_DATE(); - *end += gregorian::days(1); - } - return; + else if (word == _("today") || word == _("day")) { + duration = gregorian::days(1); } + else { + parse_specifier = true; + } + end = date_interval_t::add_duration(start, duration); - parse_inclusion_specifier(word, begin, end); + if (parse_specifier) + parse_inclusion_specifier(word, &start, &end); if (type == _("last")) { - if (mon_spec) { - if (begin) - *begin = interval_t(0, -1, 0).increment(*begin); - if (end) - *end = interval_t(0, -1, 0).increment(*end); - } else { - if (begin) - *begin = interval_t(0, 0, -1).increment(*begin); - if (end) - *end = interval_t(0, 0, -1).increment(*end); - } + start = date_interval_t::subtract_duration(start, duration); + end = date_interval_t::subtract_duration(end, duration); } else if (type == _("next")) { - if (mon_spec) { - if (begin) - *begin = interval_t(0, 1, 0).increment(*begin); - if (end) - *end = interval_t(0, 1, 0).increment(*end); - } else { - if (begin) - *begin = interval_t(0, 0, 1).increment(*begin); - if (end) - *end = interval_t(0, 0, 1).increment(*end); - } + start = date_interval_t::add_duration(start, duration); + end = date_interval_t::add_duration(end, duration); } + + if (look_for_start) interval.start = start; + if (look_for_end) interval.end = end; } } -void interval_t::parse(std::istream& in) +void date_interval_t::parse(std::istream& in) { string word; @@ -327,65 +532,62 @@ void interval_t::parse(std::istream& in) int quantity = lexical_cast<int>(word); read_lower_word(in, word); if (word == _("days")) - days = quantity; - else if (word == _("weeks")) { - days = 7 * quantity; - weekly = true; - } + duration = gregorian::days(quantity); + else if (word == _("weeks")) + duration = gregorian::weeks(quantity); else if (word == _("months")) - months = quantity; + duration = gregorian::months(quantity); else if (word == _("quarters")) - months = 3 * quantity; + duration = gregorian::months(3 * quantity); else if (word == _("years")) - years = quantity; + duration = gregorian::years(quantity); } else if (word == _("day")) - days = 1; - else if (word == _("week")) { - days = 7; - weekly = true; - } + duration = gregorian::days(1); + else if (word == _("week")) + duration = gregorian::weeks(1); else if (word == _("month")) - months = 1; + duration = gregorian::months(1); else if (word == _("quarter")) - months = 3; + duration = gregorian::months(3); else if (word == _("year")) - years = 1; + duration = gregorian::years(1); } else if (word == _("daily")) - days = 1; - else if (word == _("weekly")) { - days = 7; - weekly = true; - } + duration = gregorian::days(1); + else if (word == _("weekly")) + duration = gregorian::weeks(1); else if (word == _("biweekly")) - days = 14; + duration = gregorian::weeks(2); else if (word == _("monthly")) - months = 1; + duration = gregorian::months(1); else if (word == _("bimonthly")) - months = 2; + duration = gregorian::months(2); else if (word == _("quarterly")) - months = 3; + duration = gregorian::months(3); else if (word == _("yearly")) - years = 1; + duration = gregorian::years(1); else if (word == _("this") || word == _("last") || word == _("next") || word == _("today")) { - parse_date_words(in, word, &begin, &end); + parse_date_words(in, word, *this); } else if (word == _("in")) { read_lower_word(in, word); - parse_date_words(in, word, &begin, &end); + parse_date_words(in, word, *this); } else if (word == _("from") || word == _("since")) { read_lower_word(in, word); - parse_date_words(in, word, &begin, NULL); + parse_date_words(in, word, *this, true, false); } else if (word == _("to") || word == _("until")) { read_lower_word(in, word); - parse_date_words(in, word, NULL, &end); + parse_date_words(in, word, *this, false, true); } else { - parse_inclusion_specifier(word, &begin, &end); + date_t b, e; + parse_inclusion_specifier(word, &b, &e); + start = b; + end = e; } } } diff --git a/src/times.h b/src/times.h index b83c65b5..9750bbfb 100644 --- a/src/times.h +++ b/src/times.h @@ -60,6 +60,7 @@ inline bool is_valid(const datetime_t& moment) { typedef boost::gregorian::date date_t; typedef boost::gregorian::date_duration date_duration_t; +typedef boost::gregorian::date_iterator date_iterator_t; inline bool is_valid(const date_t& moment) { return ! moment.is_not_a_date(); @@ -75,7 +76,7 @@ inline bool is_valid(const date_t& moment) { extern int start_of_week; extern optional<std::string> input_date_format; -int string_to_day_of_week(const std::string& str); +date_time::weekdays string_to_day_of_week(const std::string& str); datetime_t parse_datetime(const char * str, int current_year = -1); @@ -123,65 +124,91 @@ inline std::string format_date(const date_t& when, return buf; } -/** - * @brief Brief - * - * Long. - */ -struct interval_t +class date_interval_t : public equality_comparable<date_interval_t> { - int years; - int months; - int days; - bool weekly; - date_t begin; - date_t end; - - interval_t(int _days = 0, - int _months = 0, - int _years = 0, - bool _weekly = false, - const date_t& _begin = date_t(), - const date_t& _end = date_t()) - : years(_years), months(_months), days(_days), - weekly(_weekly), begin(_begin), end(_end) { - TRACE_CTOR(interval_t, - "int, int, int, bool, const date_t&, const date_t&"); +public: + typedef variant<gregorian::days, + gregorian::weeks, + gregorian::months, + gregorian::years> duration_t; + + static date_t add_duration(const date_t& date, + const duration_t& duration); + static date_t subtract_duration(const date_t& date, + const duration_t& duration); + + optional<date_t> start; + bool aligned; + optional<duration_t> skip_duration; + std::size_t factor; + optional<date_t> next; + optional<duration_t> duration; + optional<date_t> end_of_duration; + optional<date_t> end; + + explicit date_interval_t() : aligned(false), factor(1) { + TRACE_CTOR(date_interval_t, ""); } - interval_t(const interval_t& other) - : years(other.years), - months(other.months), - days(other.days), - weekly(other.weekly), - begin(other.begin), + date_interval_t(const string& str) : aligned(false), factor(1) { + TRACE_CTOR(date_interval_t, "const string&"); + parse(str); + } + date_interval_t(const date_interval_t& other) + : start(other.start), + aligned(other.aligned), + skip_duration(other.skip_duration), + factor(other.factor), + next(other.next), + duration(other.duration), + end_of_duration(other.end_of_duration), end(other.end) { - TRACE_CTOR(interval_t, "copy"); + TRACE_CTOR(date_interval_t, "copy"); } - interval_t(const string& desc) - : years(0), months(0), days(0), weekly(false), begin(), end() { - TRACE_CTOR(interval_t, "const string&"); - std::istringstream stream(desc); - parse(stream); + ~date_interval_t() throw() { + TRACE_DTOR(date_interval_t); } - ~interval_t() throw() { - TRACE_DTOR(interval_t); + bool operator==(const date_interval_t& other) const { + return (start == other.start && + (! start || *start == *other.start)); } operator bool() const { - return years != 0 || months != 0 || days != 0; + return is_valid(); } - void set_start(const date_t& moment) { - begin = first(moment); + void parse(std::istream& in); + + void parse(const string& str) { + std::istringstream in(str); + parse(in); } - date_t first(const optional<date_t>& moment = none); - date_t increment(const date_t&) const; + void resolve_end(); + void stabilize(const optional<date_t>& date = none); - void parse(std::istream& in); + bool is_valid() const { + return start; + } + + /** Find the current or next period containing date. Returns true if the + date_interval_t object has been altered to reflect the interval + containing date, or false if no such period can be found. */ + bool find_period(const date_t& date); + + optional<date_t> inclusive_end() const { + if (end_of_duration) + return *end_of_duration - gregorian::days(1); + else + return none; + } + + date_interval_t& operator++(); }; +std::ostream& operator<<(std::ostream& out, + const date_interval_t::duration_t& duration); + } // namespace ledger #endif // _TIMES_H diff --git a/src/token.cc b/src/token.cc index b7c19ceb..fde6a0e3 100644 --- a/src/token.cc +++ b/src/token.cc @@ -205,9 +205,12 @@ void expr_t::token_t::next(std::istream& in, const uint_least8_t pflags) in.get(c); length++; - interval_t timespan(buf); + date_interval_t timespan(buf); + if (! timespan) + throw_(parse_error, + _("Date specifier does not refer to a starting date")); kind = VALUE; - value = timespan.first(); + value = *timespan.start; break; } @@ -186,8 +186,8 @@ struct auto_xact_finalizer_t : public xact_finalizer_t class period_xact_t : public xact_base_t { public: - interval_t period; - string period_string; + date_interval_t period; + string period_string; period_xact_t() { TRACE_CTOR(period_xact_t, ""); @@ -206,7 +206,7 @@ class period_xact_t : public xact_base_t } virtual bool valid() const { - return period; + return period.is_valid(); } }; |