2

I need to parse time from string (%Y-%M-%d %H:%m:%s) according to some timezone.

My first idea was to try boost::date_time, however it looks like its database is outdated and timezone detection algorithm is wrong in general. So I decided to try boost::locale. It has ICU backend, so timezone support should be good. I use the following code:

namespace as = boost::locale::as;

void foo(std::string time, std::string timezone) {
    auto glob = boost::locale::localization_backend_manager::global();
    glob.select("icu"); // select icu backend
    boost::locale::generator gen{glob};

    auto loc = gen.generate(""); // generate locale with boost facets
    auto cal = boost::locale::calendar{loc, timezone};

    boost::locale::date_time dt{cal};

    std::stringstream ss{time};
    ss.imbue(loc);
    std::cout.imbue(loc);

    ss >> as::ftime("%Y-%m-%d %T") >> as::time_zone(timezone) >> dt;
    std::cout << as::time_zone("UTC") << dt << std::endl;
    std::cout << as::time_zone(timezone) << dt << std::endl;
}

This works well, however if I pass some invalid timezone name ("foo"), the library accepts it, no exception is thrown, the time is parsed as if it is UTC time. That's not good for me, I want to detect this case somehow, so that I can notify user that the result will not be what he/she expects.

My first idea was to check cal.get_time_zone(), but it always returns the string that was passed to constructor ("foo" in my case), no matter if it's valid or not.

Next, I tried to extract calendar_facet from the generated locale, like so:

const auto &icu_cal = std::use_facet<boost::locale::calendar_facet>(loc);

so that I can access an internal abstract_calendar class. Unfortunately, this line doesn't compile. The reason is that boost/locale/generator.hpp has a static constant with the same name (calendar_facet) in the same boost::locale namespace. The compiler reports that it can not instantiate std::use_facet. Maybe I can move it to a separate compilation unit and avoid including generator.hpp header there, but it looks like a hack for me. Is it a bug or I'm missing something here?

Is there a straightforward way how to validate timezone name with boost::locale? Do you recommend it in general? Thanks for your help.

Edit: here is a minimal example of code that doesn't compile for me

#include <boost/locale.hpp>
int main() {
  auto my = boost::locale::localization_backend_manager::global();
  my.select("icu");
  boost::locale::generator gen{my};
  std::use_facet<boost::locale::calendar_facet>(gen.generate(""));
  return 0;
}

I compile it like so (on ubuntu 16.04, gcc 5.4):

g++ -std=c++14 -L/usr/lib/x86_64-linux-gnu/ test.cpp -lboost_locale -lboost_date_time

Edit 2: With Sehe's help I managed to get calendar facet from locale and now can I check timezone like this:

int main(int argc, char **argv) {
  auto my = boost::locale::localization_backend_manager::global();
  my.select("icu");
  boost::locale::generator gen{my};
  auto ptr = std::unique_ptr<boost::locale::abstract_calendar>(std::use_facet<class boost::locale::calendar_facet>(gen.generate("")).create_calendar());
  ptr->set_timezone(argv[1]);
  // if ICU backend does not recognize timezone, it sets it to Etc/Unknown
  if (ptr->get_timezone() != argv[1]) {
      std::cout << "bad timezone " << ptr->get_timezone() << std::endl;
  } else {
      std::cout << "good timezone " << ptr->get_timezone() << std::endl;
  }
  return 0;
}

Update: while I managed to make boost locale do what I want on linux, I later faced some weird errors when I ported my code to OS X (it looks like mac doesn't have ICU backend by default...). So, I decided to switch to Howard Hinnant's date library instead. This library is of a high quality, works well on both linux and mac, author is helpful and responsive, so highly recommended.

Pavel Davydov
  • 3,379
  • 3
  • 28
  • 41
  • 1
    No samples? No self-contained code? – sehe Sep 08 '17 at 11:36
  • I don't know about Boost Locale, but ICU itself has a similar quirk: `createTimeZone()` never returns null, but it does return the same value as `getUnknown()` so you need to compare against that every time to see if you got a "bad" timezone. See `createTimeZone()` here: http://www.icu-project.org/apiref/icu4c/classicu_1_1TimeZone.html#a35da0507b62754ffe5d8d59c19775cdb – John Zwinck Sep 08 '17 at 11:42
  • @sehe I've added an example of code that doesn't compile for me, is it ok? – Pavel Davydov Sep 08 '17 at 11:49
  • Is your question about a particular trick not compiling, or do you want reliable TZ parsing? – sehe Sep 08 '17 at 11:50
  • @JohnZwinck yes, I've read about it, but at least in icu it is possible to compare against `getUnknown()`. So I thought maybe something similar is possible with boost.. – Pavel Davydov Sep 08 '17 at 11:51
  • @sehe I want reliable TZ parsing of course! well, I thought parsing this way is reliable... Am I wrong? :) – Pavel Davydov Sep 08 '17 at 11:54
  • Well. I'm looking at it, but your code doesn't even bother to check the result of `ss >> ...` - so I wouldn't expect it to be reliable. Anyways, I'm wasting all my time finding whether the `as::ftime` manipulator has any effect on `istream` (doesn't look like it with the name of the `strftime` formatting flag). – sehe Sep 08 '17 at 11:56
  • @sehe Okay, that's a good point, I should be more careful with streams here. In fact I posted a minimal example, will fix it in real code, thanks. In fact first code sample works, the only problem is that I have no idea how to check timezone name. – Pavel Davydov Sep 08 '17 at 12:02
  • To my surprise I can only confirm that it's easy to make the library do bogus things (see http://coliru.stacked-crooked.com/a/4808b95debb0c7af). I can only constructively look at something completely different that I made: https://stackoverflow.com/a/44091087/85371 – sehe Sep 08 '17 at 12:14
  • @sehe If I got it right, your code uses strptime's %Z format character, I thought about using it instead of boost and icu, however, according to `man strptime` on mac os X, it only accepts local timezone or "GMT". I have to support linux and os X in my code, so I can't relay on strptime for timezones, I'm afraid. – Pavel Davydov Sep 08 '17 at 12:38
  • Too bad. I admit I only really tested that function on Linux (FreeBSD a long time ago). Good luck with your quest! – sehe Sep 08 '17 at 12:54

2 Answers2

2

The fix to the non-compiling sample:

Live On Coliru

#include <boost/locale.hpp>
int main() {
  auto my = boost::locale::localization_backend_manager::global();
  my.select("icu");
  boost::locale::generator gen{my};
  std::use_facet<class boost::locale::calendar_facet>(gen.generate(""));
}
sehe
  • 374,641
  • 47
  • 450
  • 633
  • Please check my second update. I extract an abstract calendar from calendar facet (it is implemented in ICU backend) and get timezone name from it. What do you think, is it ok? – Pavel Davydov Sep 08 '17 at 12:13
  • @PavelDavydov that looks promising. I'd make sure that the type of the timezone you compare to is `std::string`, so as to avoid any questions in review (also, check `argc` :)). And all these things would only pass my review with exhaustive tests. But - at least you found a way forward for the TZ part – sehe Sep 08 '17 at 12:17
  • Thanks for your help! Sure, I'll fix the issues and add tests before using this code. – Pavel Davydov Sep 08 '17 at 12:30
1

Here is an alternative timezone library that may be easier to use:

#include "tz.h"
#include <iostream>
#include <sstream>

int
main(int argc, char **argv)
{
    try
    {
        auto tz = date::locate_zone(argv[1]);
        std::cout << "good timezone " << tz->name() << std::endl;
        date::local_seconds tp;
        std::istringstream in{"2017-09-08 11:30:15"};
        in >> date::parse("%Y-%m-%d %H:%M:%S", tp);
        auto zt = date::make_zoned(tz, tp);
        std::cout << date::format("%Y-%m-%d %T %Z which is ", zt);
        std::cout << date::format("%Y-%m-%d %T %Z\n", zt.get_sys_time());
    }
    catch (std::exception const& e)
    {
        std::cout << "bad timezone " << e.what() << std::endl;
    }
}

Sample output 1:

good timezone America/New_York
2017-09-08 11:30:15 EDT which is 2017-09-08 15:30:15 UTC

Sample output 2:

bad timezone America/New_Yor not found in timezone database
Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • In fact I thought about it, read the docs and I liked the api. Thanks for your work on chrono and date libs. The only thing that stops me from using date in my project is that I'm not sure how stable is it now? In my understanding it's a prototype for the future standardization, isn't it? Do you recommend using it in production already? – Pavel Davydov Sep 08 '17 at 17:10
  • The last time there was a major incompatible API change was May 21, 2016. I changed the name of `day_point` to `sys_days`. I didn't do that lightly as I had lots of clients at the time. But it was necessary, and I documented it as well as I could. A simple search/replace fixed the issue. Almost all of the other changes have been backwards compatible. If I do need to change something, I will be noisy about it, with instructions on how to port. If changes need to be tried out for standardization, I'll float them on the `std` branch before merging to `master`. – Howard Hinnant Sep 08 '17 at 17:21
  • Yes, people are using this in production, and I take that seriously. It is likely there will be further changes as this gets standardized. But this implementation and API won't go away any time soon. I will allow years of migration time from this lib to a std one should it be standardized. And the very soonest standardization could take place is 2020. – Howard Hinnant Sep 08 '17 at 17:21
  • 1
    I tried date library and decided to switch to it from boost locale. Date library is much cleaner and easier to use. So, I'll mark this as answer. Thanks. – Pavel Davydov Oct 31 '17 at 10:11