<NME> 55831A79.test
<BEF> 2008/01/01 * previous balances
    liabilities:credit cards:discover 4462            $-1094.38
    equity:opening balances

2008/01/18 * DISCOVER FEES
    liabilities:credit cards:discover 4462:interest          $-36.59
    assets:bank:wells fargo:checking

2008/01/18 * DISCOVER FEES
    liabilities:credit cards:discover 4462:fee          $-39
    liabilities:credit cards:discover 4462:interest    $-28.17
    assets:bank:wells fargo:checking
    liabilities:credit cards:discover 4462:interest    $-28.17
    assets:bank:wells fargo:checking

2008/3/1 * discover card payment
    liabilities:credit cards:discover 4462        $1198.14
    assets:bank:wells fargo:checking
>>>1
             $-39.00    fee
             $-64.76    interest
--------------------
                   0
end test

<MSG> Removed reliance on strptime/strftime

The code now uses Boost's input and output facets for times and dates.
This ensures completely consistency regarding timezones and times, and
fixes the regression test that was broken while I was away coding in
London (where it was GMT-0 and I didn't notice the difference between
local and GMT).

<DFF> @@ -13,7 +13,7 @@ bal discover
     liabilities:credit cards:discover 4462:interest    $-28.17
     assets:bank:wells fargo:checking
 
-2008/3/1 * discover card payment
+2008/03/01 * discover card payment
     liabilities:credit cards:discover 4462        $1198.14
     assets:bank:wells fargo:checking
 >>>1

{'commit': 'diff --git a/src/item.cc b/src/item.cc\nindex 9e297052..54f36e11 100644\n--- a/src/item.cc\n+++ b/src/item.cc\n@@ -114,7 +114,7 @@ void item_t::set_tag(const string&           tag,\n   assert(result.second);\n }\n \n-void item_t::parse_tags(const char * p, int current_year)\n+void item_t::parse_tags(const char * p, optional<date_t::year_type> current_year)\n {\n   if (const char * b = std::strchr(p, \'[\')) {\n     if (const char * e = std::strchr(p, \']\')) {\n@@ -160,7 +160,7 @@ void item_t::parse_tags(const char * p, int current_year)\n   }\n }\n \n-void item_t::append_note(const char * p, int current_year)\n+void item_t::append_note(const char * p, optional<date_t::year_type> current_year)\n {\n   if (note) {\n     *note += \'\\n\';\ndiff --git a/src/item.h b/src/item.h\nindex 1d9a63c7..7855d6b7 100644\n--- a/src/item.h\n+++ b/src/item.h\n@@ -129,8 +129,10 @@ public:\n   virtual void set_tag(const string& tag,\n \t\t       const optional<string>& value = none);\n \n-  virtual void parse_tags(const char * p, int current_year = -1);\n-  virtual void append_note(const char * p, int current_year = -1);\n+  virtual void parse_tags(const char * p,\n+\t\t\t  optional<date_t::year_type> current_year = none);\n+  virtual void append_note(const char * p,\n+\t\t\t   optional<date_t::year_type> current_year = none);\n \n   static bool use_effective_date;\n \ndiff --git a/src/session.h b/src/session.h\nindex 5d6a12b9..894c59fa 100644\n--- a/src/session.h\n+++ b/src/session.h\n@@ -67,7 +67,7 @@ class session_t : public symbol_scope_t\n \n public:\n   bool flush_on_next_data_file;\n-  int  current_year;\n+  date_t::year_type current_year;\n \n   shared_ptr<commodity_pool_t> commodity_pool;\n   scoped_ptr<account_t>\t       master;\ndiff --git a/src/system.hh.in b/src/system.hh.in\nindex 2358ddcd..1f4a7d63 100644\n--- a/src/system.hh.in\n+++ b/src/system.hh.in\n@@ -53,6 +53,7 @@\n #include <algorithm>\n #include <exception>\n #include <typeinfo>\n+#include <locale>\n #include <stdexcept>\n #include <iostream>\n #include <streambuf>\n@@ -98,7 +99,6 @@ typedef std::ostream::pos_type ostream_pos_type;\n #include <cstdio>\n #include <cstdlib>\n #include <cstring>\n-#include <ctime>\n #include <csignal>\n \n #if defined __FreeBSD__ && __FreeBSD__ <= 4\n@@ -144,6 +144,8 @@ typedef std::ostream::pos_type ostream_pos_type;\n #include <boost/cast.hpp>\n #include <boost/current_function.hpp>\n #include <boost/date_time/posix_time/posix_time.hpp>\n+#include <boost/date_time/posix_time/posix_time_io.hpp>\n+#include <boost/date_time/gregorian/gregorian_io.hpp>\n #include <boost/filesystem/convenience.hpp>\n #include <boost/filesystem/exception.hpp>\n #include <boost/filesystem/fstream.hpp>\ndiff --git a/src/textual.cc b/src/textual.cc\nindex 35fa0028..967e2f1b 100644\n--- a/src/textual.cc\n+++ b/src/textual.cc\n@@ -65,7 +65,6 @@ namespace {\n     account_t *\t      master;\n     const path *      original_file;\n     accounts_map      account_aliases;\n-    int               current_year;\n     bool              strict;\n \n     path\t      pathname;\n@@ -76,6 +75,8 @@ namespace {\n     std::size_t       count;\n     std::size_t       errors;\n \n+    optional<date_t::year_type> current_year;\n+\n     scoped_ptr<auto_xact_finalizer_t> auto_xact_finalizer;\n \n     instance_t(std::list<account_t *>& _account_stack,\n@@ -476,7 +477,7 @@ void instance_t::nomarket_directive(char * line)\n \n void instance_t::year_directive(char * line)\n {\n-  current_year = lexical_cast<int>(skip_ws(line + 1));\n+  current_year = lexical_cast<unsigned short>(skip_ws(line + 1));\n }\n \n void instance_t::option_directive(char * line)\ndiff --git a/src/times.cc b/src/times.cc\nindex deb51058..45f7ed10 100644\n--- a/src/times.cc\n+++ b/src/times.cc\n@@ -41,64 +41,83 @@ std::string\t      output_datetime_format = "%Y-%m-%d %H:%M:%S";\n std::string\t      output_date_format     = "%Y-%m-%d";\n \n namespace {\n-  const char * formats[] = {\n-    "%y/%m/%d",\n-    "%Y/%m/%d",\n-    "%m/%d",\n-    "%Y/%m",\n-    "%y.%m.%d",\n-    "%Y.%m.%d",\n-    "%m.%d",\n-    "%Y.%m",\n-    "%y-%m-%d",\n-    "%Y-%m-%d",\n-    "%m-%d",\n-    "%Y-%m",\n-    NULL\n+  struct date_format_t {\n+    const char * format;\n+    bool\t has_year;\n+    date_format_t(const char * _format, bool _has_year)\n+      : format(_format), has_year(_has_year) {}\n   };\n \n-  bool parse_date_mask(const char * date_str, std::tm& result)\n-  {\n-    if (input_date_format) {\n-      std::memset(&result, -1, sizeof(std::tm));\n-      if (strptime(date_str, input_date_format->c_str(), &result))\n-\treturn true;\n-    }\n-    for (const char ** f = formats; *f; f++) {\n-      std::memset(&result, -1, sizeof(std::tm));\n-      if (strptime(date_str, *f, &result))\n-\treturn true;\n-    }\n-    return false;\n-  }\n+  const date_format_t formats[] = {\n+    date_format_t("%m/%d",    false),\n+    date_format_t("%Y/%m/%d", true),\n+    date_format_t("%Y/%m",    true),\n+    date_format_t("%y/%m/%d", true),\n+    date_format_t("%m.%d",    false),\n+    date_format_t("%Y.%m.%d", true),\n+    date_format_t("%Y.%m",    true),\n+    date_format_t("%y.%m.%d", true),\n+    date_format_t("%m-%d",    false),\n+    date_format_t("%Y-%m-%d", true),\n+    date_format_t("%Y-%m",    true),\n+    date_format_t("%y-%m-%d", true)\n+  };\n \n-  bool quick_parse_date(const char * date_str, std::tm& result, const int year)\n+  date_t parse_date_mask_routine(const char * date_str, const date_format_t& df,\n+\t\t\t\t optional<date_t::year_type> year, bool& saw_year)\n   {\n-    if (! parse_date_mask(date_str, result))\n-      return false;\n+    std::string str(date_str);\n \n-    result.tm_hour = 0;\n-    result.tm_min  = 0;\n-    result.tm_sec  = 0;\n+    gregorian::date_input_facet * facet(new gregorian::date_input_facet(df.format));\n+    std::istringstream sstr(str);\n+    sstr.imbue(std::locale(sstr.getloc(), facet));\n \n-    if (result.tm_mday == -1)\n-      result.tm_mday = 1;\n+    date_t when;\n+    sstr >> when;\n \n-    if (result.tm_mon == -1) {\n-      result.tm_mon = 0;\n+    if (! when.is_not_a_date()) {\n+      if (sstr.good() && ! sstr.eof() && sstr.peek() != EOF)\n+\treturn date_t();\n \n-      if (result.tm_mday > (CURRENT_DATE().day() - 1))\n-\tresult.tm_mon = 11;\n+      DEBUG("times.parse", "Parsed date string: " << date_str);\n+      DEBUG("times.parse", "Parsed result is: " << when);\n+      DEBUG("times.parse", "Format used was: " << df.format);\n+\n+      if (! df.has_year) {\n+\tsaw_year = false;\n+\n+\twhen = date_t(year ? *year : CURRENT_DATE().year(),\n+\t\t      when.month(), when.day());\n+\n+\tif (when.month() > CURRENT_DATE().month())\n+\t  when -= gregorian::years(1);\n+      } else {\n+\tsaw_year = true;\n+      }\n     }\n+    return when;\n+  }\n \n-    if (result.tm_year == -1) {\n-      result.tm_year = (year == -1 ? int(CURRENT_DATE().year()) : year) - 1900;\n+  date_t parse_date_mask(const char * date_str, optional<date_t::year_type> year,\n+\t\t\t bool& saw_year)\n+  {\n+    if (input_date_format) {\n+      date_format_t df(input_date_format->c_str(), true);\n+      if (! icontains(*input_date_format, "%y"))\n+\tdf.has_year = false;\n+      date_t when = parse_date_mask_routine(date_str, df, year, saw_year);\n+      if (! when.is_not_a_date())\n+\treturn when;\n+    }\n \n-      if (year == -1 && result.tm_mon > (CURRENT_DATE().month() - 1))\n-\tresult.tm_year--;\n+    for (uint8_t i = 0; i < (sizeof(formats) / sizeof(date_format_t)); i++) {\n+      date_t when = parse_date_mask_routine(date_str, formats[i], year,\n+\t\t\t\t\t    saw_year);\n+      if (! when.is_not_a_date())\n+\treturn when;\n     }\n \n-    return true;\n+    return date_t();\n   }\n }\n \n@@ -153,21 +172,24 @@ string_to_month_of_year(const std::string& str)\n     return none;\n }\n \n-datetime_t parse_datetime(const char * str, int)\n+datetime_t parse_datetime(const char * str, optional<date_t::year_type>)\n {\n-  std::tm when;\n-  std::memset(&when, -1, sizeof(std::tm));\n-  if (strptime(str, "%Y/%m/%d %H:%M:%S", &when))\n-    return posix_time::ptime_from_tm(when);\n-  else\n-    return datetime_t();\n+  posix_time::time_input_facet * facet\n+    (new posix_time::time_input_facet("%Y/%m/%d %H:%M:%S"));\n+\n+  std::string temp(str);\n+  std::istringstream sstr(temp);\n+  sstr.imbue(std::locale(sstr.getloc(), facet));\n+\n+  datetime_t when;\n+  sstr >> when;\n+  return when;\n }\n \n-date_t parse_date(const char * str, int current_year)\n+date_t parse_date(const char * str, optional<date_t::year_type> current_year)\n {\n-  std::tm when;\n-  quick_parse_date(str, when, current_year);\n-  return gregorian::date_from_tm(when);\n+  bool saw_year;\n+  return parse_date_mask(str, current_year, saw_year);\n }\n \n date_t date_interval_t::add_duration(const date_t&     date,\n@@ -436,52 +458,24 @@ namespace {\n \t\t\t\t date_t *      begin,\n \t\t\t\t date_t *      end)\n   {\n-    struct std::tm when;\n+    bool   saw_year = true;\n+    date_t when\t    = parse_date_mask(word.c_str(), none, saw_year);\n \n-    if (! parse_date_mask(word.c_str(), when))\n+    if (when.is_not_a_date())\n       throw_(date_error, _("Could not parse date mask: %1") << word);\n \n-    when.tm_hour   = 0;\n-    when.tm_min\t   = 0;\n-    when.tm_sec\t   = 0;\n-    when.tm_isdst  = -1;\n-\n-    bool saw_year = true;\n-    bool saw_mon  = true;\n-    bool saw_day  = true;\n-\n-    if (when.tm_year == -1) {\n-      when.tm_year = CURRENT_DATE().year() - 1900;\n-      saw_year = false;\n-    }\n-    if (when.tm_mon == -1) {\n-      when.tm_mon = 0;\n-      saw_mon = false;\n-    } else {\n-      saw_year = false;\t\t// don\'t increment by year if month used\n-    }\n-    if (when.tm_mday == -1) {\n-      when.tm_mday = 1;\n-      saw_day = false;\n-    } else {\n-      saw_mon  = false;\t\t// don\'t increment by month if day used\n-      saw_year = false;\t\t// don\'t increment by year if day used\n-    }\n-\n     if (begin) {\n-      *begin = gregorian::date_from_tm(when);\n+      *begin = when;\n \n       if (end) {\n \tif (saw_year)\n \t  *end = *begin + gregorian::years(1);\n-\telse if (saw_mon)\n+\telse\n \t  *end = *begin + gregorian::months(1);\n-\telse if (saw_day)\n-\t  *end = *begin + gregorian::days(1);\n       }\n     }\n     else if (end) {\n-      *end = gregorian::date_from_tm(when);\n+      *end = when;\n     }\n   }\n \ndiff --git a/src/times.h b/src/times.h\nindex 1ff98325..247c9393 100644\n--- a/src/times.h\n+++ b/src/times.h\n@@ -81,39 +81,34 @@ string_to_day_of_week(const std::string& str);\n optional<date_time::months_of_year>\n string_to_month_of_year(const std::string& str);\n \n-datetime_t parse_datetime(const char * str, int current_year = -1);\n+datetime_t parse_datetime(const char * str,\n+\t\t\t  optional<date_t::year_type> current_year = none);\n \n inline datetime_t parse_datetime(const std::string& str,\n-\t\t\t\t int current_year = -1) {\n+\t\t\t\t optional<date_t::year_type> current_year = none) {\n   return parse_datetime(str.c_str(), current_year);\n }\n \n-date_t parse_date(const char * str, int current_year = -1);\n+date_t parse_date(const char * str,\n+\t\t  optional<date_t::year_type> current_year = none);\n \n-inline date_t parse_date(const std::string& str, int current_year = -1) {\n+inline date_t parse_date(const std::string& str,\n+\t\t\t optional<date_t::year_type> current_year = none) {\n   return parse_date(str.c_str(), current_year);\n }\n \n-inline std::time_t to_time_t(const ptime& t) \n-{ \n-  if( t == posix_time::neg_infin ) \n-    return 0; \n-  else if( t == posix_time::pos_infin ) \n-    return LONG_MAX; \n-  ptime start(date(1970,1,1)); \n-  return (t-start).total_seconds(); \n-}\n-\n extern std::string output_datetime_format;\n \n inline std::string format_datetime(const datetime_t& when,\n \t\t\t\t   const optional<std::string>& format = none)\n {\n-  char buf[256];\n-  std::time_t moment = to_time_t(when);\n-  std::strftime(buf, 255, format ? format->c_str() :\n-\t\toutput_datetime_format.c_str(), std::localtime(&moment));\n-  return buf;\n+  posix_time::time_facet * facet\n+    (new posix_time::time_facet(format ? format->c_str() :\n+\t\t\t\toutput_datetime_format.c_str()));\n+  std::ostringstream buf;\n+  buf.imbue(std::locale(std::locale::classic(), facet));\n+  buf << when;\n+  return buf.str();\n }\n \n extern std::string output_date_format;\n@@ -121,11 +116,13 @@ extern std::string output_date_format;\n inline std::string format_date(const date_t& when,\n \t\t\t       const optional<std::string>& format = none)\n {\n-  char buf[256];\n-  std::tm moment = gregorian::to_tm(when);\n-  std::strftime(buf, 255, format ? format->c_str() :\n-\t\toutput_date_format.c_str(), &moment);\n-  return buf;\n+  gregorian::date_facet * facet\n+    (new gregorian::date_facet(format ? format->c_str() :\n+\t\t\t       output_date_format.c_str()));\n+  std::ostringstream buf;\n+  buf.imbue(std::locale(std::locale::classic(), facet));\n+  buf << when;\n+  return buf.str();\n }\n \n class date_interval_t : public equality_comparable<date_interval_t>\ndiff --git a/test/regress/461980A1.test b/test/regress/461980A1.test\nindex d78b5fb7..bbf2ee4e 100644\n--- a/test/regress/461980A1.test\n+++ b/test/regress/461980A1.test\n@@ -1,6 +1,6 @@\n bal\n <<<\n-2008/1/1 one\n+2008/01/01 one\n   test:a  1\n   test:b\n >>>1\ndiff --git a/test/regress/55831A79.test b/test/regress/55831A79.test\nindex 3f4a6348..d553bdfa 100644\n--- a/test/regress/55831A79.test\n+++ b/test/regress/55831A79.test\n@@ -13,7 +13,7 @@ bal discover\n     liabilities:credit cards:discover 4462:interest    $-28.17\n     assets:bank:wells fargo:checking\n \n-2008/3/1 * discover card payment\n+2008/03/01 * discover card payment\n     liabilities:credit cards:discover 4462        $1198.14\n     assets:bank:wells fargo:checking\n >>>1\n', 'commit_subject': 'Removed reliance on strptime/strftime', 'repo_name': 'ledger/ledger', 'committer': {'name': 'John Wiegley', 'email': '3f305b4b498a0d8642698be7e34009ae75d7e01c@newartisans.com', 'time_sec': '1255252741', 'tz_offset': '-240', 'date': {'seconds': '1255252741'}}, 'commit_tree': 'b8f9279c611fe08f1b10bb885144f7c275c7561d', 'commit_message': "Removed reliance on strptime/strftime\n\nThe code now uses Boost's input and output facets for times and dates.\nThis ensures completely consistency regarding timezones and times, and\nfixes the regression test that was broken while I was away coding in\nLondon (where it was GMT-0 and I didn't notice the difference between\nlocal and GMT).\n", 'language_name': 'C', 'license': 'bsd-3-clause', 'hunks': '@@ -13,7 +13,7 @@ bal discover\n     liabilities:credit cards:discover 4462:interest    $-28.17\n     assets:bank:wells fargo:checking\n \n-2008/3/1 * discover card payment\n+2008/03/01 * discover card payment\n     liabilities:credit cards:discover 4462        $1198.14\n     assets:bank:wells fargo:checking\n >>>1\n', 'addition_count': 1, 'deletion_count': 1, 'src_file': 'a/test/regress/55831A79.test', 'tgt_file': 'b/test/regress/55831A79.test', 'file_extension': '.test', 'before_file': '2008/01/01 * previous balances\n    liabilities:credit cards:discover 4462            $-1094.38\n    equity:opening balances\n\n2008/01/18 * DISCOVER FEES\n    liabilities:credit cards:discover 4462:interest          $-36.59\n    assets:bank:wells fargo:checking\n\n2008/01/18 * DISCOVER FEES\n    liabilities:credit cards:discover 4462:fee          $-39\n    liabilities:credit cards:discover 4462:interest    $-28.17\n    assets:bank:wells fargo:checking\n    liabilities:credit cards:discover 4462:interest    $-28.17\n    assets:bank:wells fargo:checking\n\n2008/3/1 * discover card payment\n    liabilities:credit cards:discover 4462        $1198.14\n    assets:bank:wells fargo:checking\n>>>1\n             $-39.00    fee\n             $-64.76    interest\n--------------------\n                   0\nend test\n', 'message': "Removed reliance on strptime/strftime\n\nThe code now uses Boost's input and output facets for times and dates.\nThis ensures completely consistency regarding timezones and times, and\nfixes the regression test that was broken while I was away coding in\nLondon (where it was GMT-0 and I didn't notice the difference between\nlocal and GMT).\n", 'author_name': 'John Wiegley', 'lang': 'test'}