4
\$\begingroup\$

I wrote a small header-only C++11 header for converting between various C++ time formats:

// time_utils/time_cast.hpp
#ifndef TIME_CAST_HPP
#define TIME_CAST_HPP

#include <chrono>
#include <ctime>
#include <sstream>
#include <iomanip>
#include <string>

namespace time_utils {

using chrono_time = std::chrono::time_point<std::chrono::system_clock>;

// TimeProxy
template <typename T>
class TimeProxy {
};

template <>
class TimeProxy<std::string> {
public:
    TimeProxy(std::time_t time) : _time(time) {}
    std::string operator()(const char* format) {
        std::stringstream ss;
        ss << std::put_time(std::localtime(&_time), format);
        return ss.str();
    }
private:
    std::time_t _time;
};

template <>
class TimeProxy<chrono_time> {
public:
    TimeProxy(std::time_t time) : _time(time) {}
    chrono_time operator()() {
        return std::chrono::system_clock::from_time_t(_time);
    }
private:
    std::time_t _time;
};

template <>
class TimeProxy<std::time_t> {
public:
    TimeProxy(std::time_t time) : _time(time) {}
    std::time_t operator()() {
        return _time;
    }
private:
    std::time_t _time;
};

// time_cast
template <typename Out>
TimeProxy<Out> time_cast(std::string in, const char* format) {
    // read in to std::time_t
    struct tm bk_time;  
    strptime(in.c_str(), format, &bk_time);
    std::time_t time = mktime(&bk_time);
    return TimeProxy<Out>(time);
}

template <typename Out>
TimeProxy<Out> time_cast(std::time_t time) {
    // pass time_t directly
    return TimeProxy<Out>(time);
}

template <typename Out>
TimeProxy<Out> time_cast(chrono_time in) {
    // read in to std::time_t
    std::time_t time = std::chrono::system_clock::to_time_t(in);
    return TimeProxy<Out>(time);
}

}

#endif

To cast one time to another you have to use time_utils::time_cast template function, passing target type as a template argument. The input type will be deduced:

time_utils::time_cast<output_type>(input_time)();

The following types are supported:

  • std::string
  • std::time_t
  • std::chrono::time_point<std::chrono::system_clock>

In order to read strings you also have to pass strftime format inside the first pair of parentheses:

time_utils::time_cast<std::time_t>(input_string, "%T")();

In order get strings you have to pass format string in a second pair of parentheses:

time_utils::time_cast<std::string>(input_data)("%x %X")

Example client code:

#include <iostream>
#include <chrono>
#include <ctime>

#include "time_utils/time_cast.hpp"

using time_utils::time_cast;
using chrono_time = std::chrono::time_point<std::chrono::system_clock>;

int main() {
    std::time_t time_t_time = 1459439688;
    std::chrono::time_point<std::chrono::system_clock> tmp, now = std::chrono::system_clock::from_time_t(time_t_time);
    std::string str_time("Thu Mar 31 17:54:48 2016"); // format "%c"

    // std::string to std::string (convert string format)
    std::cout << time_cast<std::string>(str_time, "%c")("%x %X") << std::endl;
    std::cout << std::endl;

    // std::chrono::time_point to std::string
    std::cout << time_cast<std::string>(now)("%x %X") << std::endl;
    std::cout << std::endl;

    // std::string to std::chrono::time_point
    tmp = time_cast<chrono_time>(str_time, "%c")();
    std::cout << time_cast<std::string>(tmp)("%x %X") << std::endl;
    std::cout << std::endl;

    // std::time_t to std::string
    std::cout << time_cast<std::string>(time_t_time)("%c") << std::endl;
    std::cout << std::endl; 

    // std::time_t to std::chrono::time_point
    tmp = time_cast<chrono_time>(time_t_time)();
    std::cout << time_cast<std::string>(tmp)("%x %X") << std::endl;
    std::cout << std::endl;  

    // std::string to std::time_t
    std::cout << time_cast<std::time_t>(str_time, "%c")() << std::endl;
    std::cout << std::endl;

    // std::chrono::time_point to std::time_t
    std::cout << time_cast<std::time_t>(now)() << std::endl;
    std::cout << std::endl;
}
\$\endgroup\$
  • \$\begingroup\$ Regarding my DRY point I stated yesterday. Well, I managed to factor out the repeated _time data member definitions here. Though it's questionable if this helps to improve your current code (and that particular case). That refactoring would help if there's more than a single data member. There's not much saved in typing at all. \$\endgroup\$ – πάντα ῥεῖ Jan 10 '17 at 19:25
  • \$\begingroup\$ The next thing I'll try to improve is giving a more concise compiler error message for unsupported type conversions: coliru.stacked-crooked.com/a/cc64de4f6e615af0 \$\endgroup\$ – πάντα ῥεῖ Jan 10 '17 at 20:31
  • \$\begingroup\$ Ouch that appears even worse regarding the compiler error messages: coliru.stacked-crooked.com/a/6cb207bf7579adf7. I think what needs to be done for improvement is said from my side so far. \$\endgroup\$ – πάντα ῥεῖ Jan 10 '17 at 20:48
2
\$\begingroup\$

Your template class looks useful, and the usage is made quite concise with the explicit declaration/definition of the time_cast() variants.

That's somehow replicating an interface like the standard cast implementations (static_cast<>(), reinterpret_cast<>(), etc.) do.


So just my 2¢ so far:

DRY

First when looking at your code, I was bothered a bit regarding the repetition of the common class layout in your specializations regarding the _time member variable.

Though, after factoring out your common data member to a common base class like this

class TimeProxyBase {
protected:
    TimeProxyBase(std::time_t time) : _time(time) {}
    std::time_t _time;
};

// TimeProxy
template <typename T>
class TimeProxy {
};

template <>
class TimeProxy<std::string> : private TimeProxyBase {
public:
    TimeProxy(std::time_t time) : TimeProxyBase(time) {}
    std::string operator()(const char* format) {
        std::stringstream ss;
        ss << std::put_time(std::localtime(&(this->_time)), format);
        return ss.str();
    }
};

template <>
class TimeProxy<chrono_time> : private TimeProxyBase {
public:
    TimeProxy(std::time_t time) : TimeProxyBase(time) {}
    chrono_time operator()() {
        return std::chrono::system_clock::from_time_t(this->_time);
    }
};

template <>
class TimeProxy<std::time_t> : private TimeProxyBase {
public:
    TimeProxy(std::time_t time) : TimeProxyBase(time) {}
    std::time_t operator()() {
        return this->_time;
    }
};

I have my doubts, if that really improves the code you have already. At least it doesn't regarding less typing.

On the other hand, if there would be more common data members I'd go that way.

The refactored code can be inspected here.

Do not use a prefix _ underscore for your own applications

The prefix underscore is preserved for C++ standard class implementations and/or compiler internals (see more detailed info here please). Rather use a postfix underscore for naming, and distinguishing member variable and parameter names:

std::time_t time_;

or

std::time_t time;

and

TimeProxy(std::time_t time_) : time(time_) {

Consider using a const reference in the constructor

Though it's very probable that time_t might be a simple positive integer number or anyways movable type, it might be better to have an explicit const reference than copy by value

TimeProxy(const std::time_t& time_);

for clarity of semantics and clearly no need for copying (at point of passing the parameter).

I know this is heavily discussed and passing by value seems to be preferred (because of standard moving behaviors should apply). Though I personally prefer the const reference style and the actual type of time_t is unspecified according this reference.

Using a const reference shouldn't do any harm anyways (besides typing a bit more).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.