/* * 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 #include "times.h" namespace ledger { optional epoch; date_time::weekdays start_of_week = gregorian::Sunday; //#define USE_BOOST_FACETS 1 namespace { template class temporal_io_t : public noncopyable { const char * fmt_str; #if defined(USE_BOOST_FACETS) std::istringstream input_stream; std::ostringstream output_stream; InputFacetType * input_facet; OutputFacetType * output_facet; std::string temp_string; #endif // USE_BOOST_FACETS public: bool has_year; bool input; temporal_io_t(const char * _fmt_str, bool _input) : fmt_str(_fmt_str), has_year(icontains(fmt_str, "%y")), input(_input) { #if defined(USE_BOOST_FACETS) if (input) { input_facet = new InputFacetType(fmt_str); input_stream.imbue(std::locale(std::locale::classic(), input_facet)); } else { output_facet = new OutputFacetType(fmt_str); output_stream.imbue(std::locale(std::locale::classic(), output_facet)); } #endif // USE_BOOST_FACETS } void set_format(const char * fmt) { fmt_str = fmt; has_year = icontains(fmt_str, "%y"); #if defined(USE_BOOST_FACETS) if (input) input_facet->format(fmt_str); else output_facet->format(fmt_str); #endif // USE_BOOST_FACETS } T parse(const char * str) { } std::string format(const T& when) { #if defined(USE_BOOST_FACETS) output_stream.str(temp_string); output_stream.seekp(std::ios_base::beg); output_stream.clear(); output_stream << when; return output_stream.str(); #else // USE_BOOST_FACETS std::tm data(to_tm(when)); char buf[128]; std::strftime(buf, 127, fmt_str, &data); return buf; #endif // USE_BOOST_FACETS } }; template <> datetime_t temporal_io_t ::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 ::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_io_t; typedef temporal_io_t date_io_t; shared_ptr input_datetime_io; shared_ptr input_date_io; shared_ptr written_datetime_io; shared_ptr written_date_io; shared_ptr printed_datetime_io; shared_ptr printed_date_io; std::vector > readers; date_t parse_date_mask_routine(const char * date_str, date_io_t& io, optional year, bool& saw_year) { date_t when; if (std::strchr(date_str, '/')) { when = io.parse(date_str); } else { char buf[128]; VERIFY(std::strlen(date_str) < 127); std::strcpy(buf, date_str); for (char * p = buf; *p; p++) if (*p == '.' || *p == '-') *p = '/'; when = io.parse(buf); } if (! when.is_not_a_date()) { DEBUG("times.parse", "Parsed date string: " << date_str); DEBUG("times.parse", "Parsed result is: " << when); if (! io.has_year) { saw_year = false; when = date_t(year ? *year : CURRENT_DATE().year(), when.month(), when.day()); if (when.month() > CURRENT_DATE().month()) when -= gregorian::years(1); } else { saw_year = true; } } return when; } date_t parse_date_mask(const char * date_str, optional year, bool& saw_year) { if (input_date_io.get()) { date_t when = parse_date_mask_routine(date_str, *input_date_io.get(), year, saw_year); if (! when.is_not_a_date()) return when; } foreach (shared_ptr& reader, readers) { date_t when = parse_date_mask_routine(date_str, *reader.get(), year, saw_year); if (! when.is_not_a_date()) return when; } throw_(date_error, _("Invalid date: %1") << date_str); return date_t(); } } optional 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 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) { 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 current_year) { bool saw_year; return parse_date_mask(str, current_year, saw_year); } std::ostream& operator<<(std::ostream& out, const date_interval_t::duration_t& duration) { if (duration.quantum == date_interval_t::duration_t::DAYS) out << duration.length << " day(s)"; else if (duration.quantum == date_interval_t::duration_t::WEEKS) out << duration.length << " week(s)"; else if (duration.quantum == date_interval_t::duration_t::MONTHS) out << duration.length << " month(s)"; else { assert(duration.quantum == date_interval_t::duration_t::YEARS); out << duration.length << " year(s)"; } return out; } void date_interval_t::resolve_end() { if (start && ! end_of_duration) { end_of_duration = duration->add(*start); DEBUG("times.interval", "stabilize: end_of_duration = " << *end_of_duration); } if (end && *end_of_duration > *end) { end_of_duration = end; DEBUG("times.interval", "stabilize: end_of_duration reset to end: " << *end_of_duration); } if (! skip_duration) { skip_duration = duration; DEBUG("times.interval", "stabilize: skip_duration set to: " << *skip_duration); } if (start && ! next) { next = skip_duration->add(*start); DEBUG("times.interval", "stabilize: next set to: " << *next); } } void date_interval_t::stabilize(const optional& date) { #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 initial_start = start; optional initial_end = end; #if defined(DEBUG_ON) if (initial_start) DEBUG("times.interval", "stabilize: initial_start = " << *initial_start); if (initial_end) DEBUG("times.interval", "stabilize: initial_end = " << *initial_end); #endif date_t when = start ? *start : *date; if (duration->quantum == duration_t::MONTHS || duration->quantum == duration_t::YEARS) { DEBUG("times.interval", "stabilize: monthly or yearly duration"); start = date_t(when.year(), gregorian::Jan, 1); } else { DEBUG("times.interval", "stabilize: daily or weekly duration"); start = date_t(when - gregorian::days(400)); if (duration->quantum == duration_t::WEEKS) { // Move it to a Sunday while (start->day_of_week() != start_of_week) *start += gregorian::days(1); } } DEBUG("times.interval", "stabilize: beginning start date = " << *start); while (*start < *date) { date_interval_t next_interval(*this); ++next_interval; if (next_interval.start && *next_interval.start < *date) { *this = next_interval; } else { end_of_duration = none; next = none; break; } } DEBUG("times.interval", "stabilize: final start date = " << *start); if (initial_start && (! start || *start < *initial_start)) { resolve_end(); start = initial_start; DEBUG("times.interval", "stabilize: start reset to initial start"); } if (initial_end && (! end || *end > *initial_end)) { end = initial_end; DEBUG("times.interval", "stabilize: end reset to initial end"); } } aligned = true; } // If there is no duration, then if we've reached here the date falls // between begin and end. if (! duration) { DEBUG("times.interval", "stabilize: there was no duration given"); if (! start && ! end) throw_(date_error, _("Invalid date interval: neither start, nor end, nor duration")); } else { resolve_end(); } } bool date_interval_t::find_period(const date_t& date) { stabilize(date); if (end && date > *end) { DEBUG("times.interval", "false: date [" << date << "] > end [" << *end << "]"); return false; } if (! start) { throw_(std::runtime_error, _("Date interval is improperly initialized")); } else if (date < *start) { DEBUG("times.interval", "false: date [" << date << "] < start [" << *start << "]"); return false; } if (end_of_duration) { if (date < *end_of_duration) { DEBUG("times.interval", "true: date [" << date << "] < end_of_duration [" << *end_of_duration << "]"); return true; } } else { DEBUG("times.interval", "false: there is no end_of_duration"); return false; } // If we've reached here, it means the date does not fall into the current // interval, so we must seek another interval that does match -- unless we // pass by date in so doing, which means we shouldn't alter the current // period of the interval at all. date_t scan = *start; date_t end_of_scan = *end_of_duration; DEBUG("times.interval", "date = " << date); DEBUG("times.interval", "scan = " << scan); DEBUG("times.interval", "end_of_scan = " << end_of_scan); while (date >= scan && (! end || scan < *end)) { if (date < end_of_scan) { start = scan; end_of_duration = end_of_scan; next = none; DEBUG("times.interval", "true: start = " << *start); DEBUG("times.interval", "true: end_of_duration = " << *end_of_duration); return true; } scan = skip_duration->add(scan); end_of_scan = duration->add(scan); } return false; } date_interval_t& date_interval_t::operator++() { if (! start) throw_(date_error, _("Cannot increment an unstarted date interval")); stabilize(); if (! skip_duration || ! duration) throw_(date_error, _("Cannot increment a date interval without a duration")); assert(next); if (end && *next >= *end) { start = none; } else { start = *next; end_of_duration = duration->add(*start); } next = none; resolve_end(); return *this; } namespace { void parse_inclusion_specifier(const string& word, date_t * begin, date_t * end) { bool saw_year = true; date_t when = parse_date_mask(word.c_str(), none, saw_year); if (when.is_not_a_date()) throw_(date_error, _("Could not parse date mask: %1") << word); if (begin) { *begin = when; if (end) { if (saw_year) *end = *begin + gregorian::years(1); else *end = *begin + gregorian::months(1); } } else if (end) { *end = when; } } inline void read_lower_word(std::istream& in, string& word) { in >> word; for (string::size_type i = 0, l = word.length(); i < l; i++) word[i] = static_cast(std::tolower(word[i])); } void parse_date_words(std::istream& in, string& word, date_interval_t& interval, bool look_for_start = true, bool look_for_end = true) { string type; if (word == _("this") || word == _("last") || word == _("next")) { type = word; if (! in.eof()) read_lower_word(in, word); else word = _("month"); } else { type = _("this"); } date_t start = CURRENT_DATE(); date_t end; bool parse_specifier = false; optional duration; assert(look_for_start || look_for_end); if (word == _("year")) { duration = date_interval_t::duration_t(date_interval_t::duration_t::YEARS, 1); start = gregorian::date(start.year(), 1, 1); } else if (word == _("month")) { duration = date_interval_t::duration_t(date_interval_t::duration_t::MONTHS, 1); start = gregorian::date(start.year(), start.month(), 1); } else if (word == _("today") || word == _("day")) { duration = date_interval_t::duration_t(date_interval_t::duration_t::DAYS, 1); } else { parse_specifier = true; } if (parse_specifier) parse_inclusion_specifier(word, &start, &end); else end = duration->add(start); if (type == _("last") && duration) { start = duration->subtract(start); end = duration->subtract(end); } else if (type == _("next") && duration) { start = duration->add(start); end = duration->add(end); } if (look_for_start && is_valid(start)) interval.start = start; if (look_for_end && is_valid(end)) interval.end = end; } } void date_interval_t::parse(std::istream& in) { string word; optional mon; optional wday; optional 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(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 m = string_to_month_of_year(word)) { mon = m; } else if (optional d = string_to_day_of_week(word)) { wday = d; } else if (all(word, is_digit())) { year = lexical_cast(word); } else { // otherwise, it should be an explicit date date_t b, e; parse_inclusion_specifier(word, &b, &e); start = b; end = e; } } if (year || mon || wday) { if (! start) start = CURRENT_DATE(); if (wday) { while (start->day_of_week() != *wday) *start = duration_t(duration_t::DAYS, 1).subtract(*start); if (! end) end = duration_t(duration_t::DAYS, 1).add(*start); } else { bool overwrite_end = false; if (year) { start = date_t(*year, 1, 1); if (! end) { end = duration_t(duration_t::YEARS, 1).add(*start); overwrite_end = true; } } if (mon) { start = date_t(start->year(), *mon, 1); if (! end || overwrite_end) end = duration_t(duration_t::MONTHS, 1).add(*start); } } } } namespace { typedef std::map datetime_io_map; typedef std::map 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& format) { if (format_type == FMT_WRITTEN) { return written_datetime_io->format(when); } else if (format_type == FMT_CUSTOM || format) { datetime_io_map::iterator i = temp_datetime_io.find(*format); if (i != temp_datetime_io.end()) { return (*i).second->format(when); } else { datetime_io_t * formatter = new datetime_io_t(*format, false); temp_datetime_io.insert(datetime_io_map::value_type(*format, formatter)); return formatter->format(when); } } else if (format_type == FMT_PRINTED) { return printed_datetime_io->format(when); } else { assert(0); return ""; } } std::string format_date(const date_t& when, const format_type_t format_type, const optional& format) { if (format_type == FMT_WRITTEN) { return written_date_io->format(when); } else if (format_type == FMT_CUSTOM || format) { date_io_map::iterator i = temp_date_io.find(*format); if (i != temp_date_io.end()) { return (*i).second->format(when); } else { date_io_t * formatter = new date_io_t(*format, false); temp_date_io.insert(date_io_map::value_type(*format, formatter)); return formatter->format(when); } } else if (format_type == FMT_PRINTED) { return printed_date_io->format(when); } else { assert(0); return ""; } } namespace { bool is_initialized = false; } void set_datetime_format(const char * format) { printed_datetime_io->set_format(format); } void set_date_format(const char * format) { printed_date_io->set_format(format); } void set_input_date_format(const char * format) { input_date_io.reset(new date_io_t(format, true)); } void times_initialize() { if (! is_initialized) { input_datetime_io.reset(new datetime_io_t("%Y/%m/%d %H:%M:%S", true)); written_datetime_io.reset(new datetime_io_t("%Y/%m/%d %H:%M:%S", false)); written_date_io.reset(new date_io_t("%Y/%m/%d", false)); printed_datetime_io.reset(new datetime_io_t("%y-%b-%d %H:%M:%S", false)); printed_date_io.reset(new date_io_t("%y-%b-%d", false)); readers.push_back(shared_ptr(new date_io_t("%m/%d", true))); readers.push_back(shared_ptr(new date_io_t("%Y/%m/%d", true))); readers.push_back(shared_ptr(new date_io_t("%Y/%m", true))); readers.push_back(shared_ptr(new date_io_t("%y/%m/%d", true))); is_initialized = true; } } void times_shutdown() { if (is_initialized) { input_datetime_io.reset(); input_date_io.reset(); written_datetime_io.reset(); written_date_io.reset(); printed_datetime_io.reset(); printed_date_io.reset(); readers.clear(); foreach (datetime_io_map::value_type& pair, temp_datetime_io) checked_delete(pair.second); temp_datetime_io.clear(); foreach (date_io_map::value_type& pair, temp_date_io) checked_delete(pair.second); temp_date_io.clear(); is_initialized = false; } } } // namespace ledger