I've developed a set of functions to convert a std::ratio
or std::chrono::duration
to a std::string
or insert it into a std::ostream
using operator<<
. They are intended to be part of a larger library of conversion functions so I've included a few additional functions in the library for demonstration.
There are two versions of each function: the normal one in the convert
namespace, and one that uses a compact form of a string in the convert::compact
namespace. For example, the ratio std::milli
is normally converted to the string "milli"
and its compact form is "m"
. Not all conversions have a compact form, so in those cases the functions in convert::compact
simply call their counterparts in convert
.
Conversions of std::chrono::duration
s take advantage of the conversions for std::ratio
s if the period ratio matches a std::ratio
typedef provided by the standard library. Similarly, proper conversions are implemented for period ratios which match a std::chrono::duration
typedef provided by the standard library (e.g. std::chrono::minutes
and std::chrono::hours
).
Here is the header file of functions, convert.h:
/** \file
\brief Functions for the conversion of types to `std::string` or for inserting them into a `std::ostream`.
*/
#ifndef CONVERT_H
#define CONVERT_H
#include <chrono>
#include <cstdint>
#include <iostream>
#include <ratio>
#include <sstream>
#include <stdexcept>
#include <string>
/** \brief Namespace for the conversion of types to `std::string` or for inserting them into a
`std::ostream`.
*/
namespace convert {
/** \brief Converts a type `T` to a `std::string` using `std::ostringstream`.
\throw std::invalid_argument if the type `T` cannot be converted by a `std::ostringstream`.
*/
template<typename T> inline std::string to_string(const T& t) {
std::ostringstream oss;
if (!(oss << t)) {
throw std::invalid_argument("unable to convert to std::string");
}
return oss.str();
}
/** \brief Specialization for converting to a `std::string` when the argument is already a `std::string`,
which simply returns the argument.
*/
template<> inline std::string to_string(const std::string& s) {
return s;
}
/** \brief Specialization for converting a `bool` to a `std::string` suggested at
https://isocpp.org/wiki/faq/templates#template-specialization-example.
Uses `std::boolalpha` to return "true" or "false" instead of "1" or "0".
*/
template<> inline std::string to_string<bool>(const bool& b) {
std::ostringstream oss;
oss << std::boolalpha << b;
return oss.str();
}
/** \brief Converts a `std::ratio<Num, Denom>` to `std::string`.
The `std::string` contains a numerator and denominator reduced to their lowest
terms `num` and `den`, obtained from `std::ratio<Num, Denom>::num` and `std::ratio<Num, Denom>::den`,
respectively.
\return a `std::string` of the form ' num' if `den` is equal to 1
\return a `std::string` of the form ' num/den' otherwise
*/
template<std::intmax_t Num, std::intmax_t Denom>
inline std::string to_string(const std::ratio<Num, Denom>& r) {
// Include a space before the numerator in case the ratio is preceded by another number
auto str = " " + std::to_string(std::ratio<Num, Denom>::num);
constexpr auto den = std::ratio<Num, Denom>::den;
if (den != 1) str += "/" + std::to_string(den);
return str;
}
/* std::yocto and std::zepto are defined in the standard if std::intmax_t can represent the
denominator. That's not the case here. Similarly, std::zetta and std::yotta are not
available. How to determine whether or not they are available?
*/
/*
template<> inline std::string to_string(const std::yocto& r) {
return "yocto";
}
template<> inline std::string to_string(const std::zepto& r) {
return "zepto";
}
*/
/** \brief Converts the ratio `std::atto` to a `std::string` consisting of the SI prefix name "atto".
*/
template<> inline std::string to_string(const std::atto& r) {
return "atto";
}
/** \brief Converts the ratio `std::femto` to a `std::string` consisting of the SI prefix name "femto".
*/
template<> inline std::string to_string(const std::femto& r) {
return "femto";
}
/** \brief Converts the ratio `std::pico` to a `std::string` consisting of the SI prefix name "pico".
*/
template<> inline std::string to_string(const std::pico& r) {
return "pico";
}
/** \brief Converts the ratio `std::nano` to a `std::string` consisting of the SI prefix name "nano".
*/
template<> inline std::string to_string(const std::nano& r) {
return "nano";
}
/** \brief Converts the ratio `std::micro` to a `std::string` consisting of the SI prefix name "micro".
*/
template<> inline std::string to_string(const std::micro& r) {
return "micro";
}
/** \brief Converts the ratio `std::milli` to a `std::string` consisting of the SI prefix name "milli".
*/
template<> inline std::string to_string(const std::milli& r) {
return "milli";
}
/** \brief Converts the ratio `std::centi` to a `std::string` consisting of the SI prefix name "centi".
*/
template<> inline std::string to_string(const std::centi& r) {
return "centi";
}
/** \brief Converts the ratio `std::deci` to a `std::string` consisting of the SI prefix name "deci".
*/
template<> inline std::string to_string(const std::deci& r) {
return "deci";
}
/** \brief Converts the ratio `std::deca` to a `std::string` consisting of the SI prefix name "deca".
*/
template<> inline std::string to_string(const std::deca& r) {
return "deca";
}
/** \brief Converts the ratio `std::hecto` to a `std::string` consisting of the SI prefix name "hecto".
*/
template<> inline std::string to_string(const std::hecto& r) {
return "hecto";
}
/** \brief Converts the ratio `std::kilo` to a `std::string` consisting of the SI prefix name "kilo".
*/
template<> inline std::string to_string(const std::kilo& r) {
return "kilo";
}
/** \brief Converts the ratio `std::mega` to a `std::string` consisting of the SI prefix name "mega".
*/
template<> inline std::string to_string(const std::mega& r) {
return "mega";
}
/** \brief Converts the ratio `std::giga` to a `std::string` consisting of the SI prefix name "giga".
*/
template<> inline std::string to_string(const std::giga& r) {
return "giga";
}
/** \brief Converts the ratio `std::tera` to a `std::string` consisting of the SI prefix name "tera".
*/
template<> inline std::string to_string(const std::tera& r) {
return "tera";
}
/** \brief Converts the ratio `std::peta` to a `std::string` consisting of the SI prefix name "peta".
*/
template<> inline std::string to_string(const std::peta& r) {
return "peta";
}
/** \brief Converts the ratio `std::exa` to a `std::string` consisting of the SI prefix name "exa".
*/
template<> inline std::string to_string(const std::exa& r) {
return "exa";
}
/*
template<> inline std::string to_string(const std::zetta& r) {
return "zetta";
}
template<> inline std::string to_string(const std::yotta& r) {
return "yotta";
}
*/
/** \brief Inserts a `std::ratio<Num, Denom>` into a `std::ostream`.
The argument is converted to a `std::string` using to_string(const std::ratio<Num, Denom>&), which is
inserted into the `std::ostream`.
*/
template<std::intmax_t Num, std::intmax_t Denom>
std::ostream& operator<<(std::ostream& os, const std::ratio<Num, Denom>& r) {
return os << convert::to_string(r);
}
/** \brief Returns a `std::string` representing the base time unit for a `std::chrono::duration`
with period type `std::ratio<num, den>`.
This is a function template so users may specialize it for a specific `std::ratio`.
\return "second", the SI base time unit
*/
template<typename std::intmax_t num, typename std::intmax_t den>
inline std::string duration_unit() {
return "second";
}
/** \brief Specialization of the `std::chrono::duration` base time unit `std::string` for the type
`std::chrono::minutes`.
\return "minute"
*/
template<> inline std::string
duration_unit<std::chrono::minutes::period::num, std::chrono::minutes::period::den>() {
return "minute";
}
/** \brief Specialization of the `std::chrono::duration` base time unit `std::string` for the type
`std::chrono::hours`.
\return "hour"
*/
template<> inline std::string
duration_unit<std::chrono::hours::period::num, std::chrono::hours::period::den>() {
return "hour";
}
/** \brief Converts a `std::chrono::duration` to a `std::string`.
\tparam Rep an arithmetic type representing the number of ticks for a `std::chrono::duration`.
\tparam Num the numerator of the `std::ratio` used as the period type of the `std::chrono::duration`.
\tparam Denom the denominator of the `std::ratio` used as the period type of the `std::chrono::duration`.
\return a `std::string` of the form "n [SI prefix][base unit]". `n` is the number of ticks in the
`std::chrono::duration` converted to a `std::string`. [base unit] is a base time unit (usually the SI
second). [SI prefix] is an SI prefix (such as "milli") which is present if the base unit is seconds
and the argument's period type is an SI ratio. The base unit is in its plural form (e.g. "seconds")
unless the `std::chrono::duration` has exactly 1 tick, in which case the base unit is singular.
*/
template<typename Rep, std::intmax_t Num, std::intmax_t Denom>
inline std::string to_string(const std::chrono::duration<Rep, std::ratio<Num, Denom> >& d) {
auto ticks = d.count();
auto str = std::to_string(ticks) + " ";
// Determine the reduced Num/Denom ratio
constexpr std::intmax_t num = std::ratio<Num, Denom>::num;
constexpr std::intmax_t den = std::ratio<Num, Denom>::den;
// Determine the base time unit (usually seconds, but may be minutes, hours, etc.)
auto unit_str = duration_unit<num, den>();
// Add the ratio prefix (e.g. "milli") if the base unit is seconds and the ratio is not 1
if (unit_str == "second" && !(num == 1 && den == 1)) {
str += to_string(std::ratio<num, den>());
}
str += unit_str;
// Pluralize if the number of ticks is not equal to exactly 1
if (ticks != 1) str += "s";
return str;
}
/** \brief Inserts a `std::chrono::duration` into a `std::ostream`.
The argument is converted to a `std::string` using
to_string(const std::chrono::duration<Rep, std::ratio<Num, Denom> >&),
which is inserted into the `std::ostream`.
*/
template<typename Rep, std::intmax_t Num, std::intmax_t Denom>
std::ostream& operator<<(std::ostream& os,
const std::chrono::duration<Rep, std::ratio<Num, Denom> >& d) {
return os << convert::to_string<Rep, Num, Denom>(d);
}
/** \brief Namespace for the conversion of types to `std::string` or for inserting them into a
`std::ostream` using a compact form.
*/
namespace compact {
/** \brief Converts a type `T` to a `std::string` using `std::ostringstream`.
In general there is no compact `std::string` form of a generic type `T` so this function simply calls
convert::to_string(const T&). This is included in the compact namespace so that users can call
`convert::compact::to_string()` on any type for which a call to `convert::to_string()` is permitted.
\throw std::invalid_argument if the type `T` cannot be converted by a `std::ostringstream`.
*/
template<typename T> inline std::string to_string(const T& t) {
return convert::to_string(t);
}
/** \brief Specialization for converting a `bool` to a `std::string`.
Uses `std::noboolalpha` to return "1" or "0".
*/
template<> inline std::string to_string<bool>(const bool& b) {
std::ostringstream oss;
oss << std::noboolalpha << b;
return oss.str();
}
/** \brief Converts a `std::ratio<Num, Denom>` to `std::string`.
The compact form is the same as the normal conversion by convert::to_string(const std::ratio<Num, Denom>&).
This is included in the compact namespace so that users can call `convert::compact::to_string()` on
any `std::ratio` and convert it properly.
*/
template<std::intmax_t Num, std::intmax_t Denom>
inline std::string to_string(const std::ratio<Num, Denom>& r) {
return convert::to_string(r);
}
/** \brief Inserts a `std::ratio<Num, Denom>` into a `std::ostream`.
The argument is converted to a `std::string` using to_string(const std::ratio<Num, Denom>&), which is
inserted into the `std::ostream`.
*/
template<std::intmax_t Num, std::intmax_t Denom>
std::ostream& operator<<(std::ostream& os, const std::ratio<Num, Denom>& r) {
return os << convert::compact::to_string(r);
}
/** \brief Returns a `std::string` representing the compact symbol form of the base time unit for a
`std::chrono::duration` with period type `std::ratio<num, den>`.
This is a function template so users may specialize it for a specific `std::ratio`.
\return "s", the SI base time unit symbol
*/
template<typename std::intmax_t num, typename std::intmax_t den>
inline std::string duration_unit() {
return "s";
}
/** \brief Specialization of the compact `std::string` form of the `std::chrono::duration` base time
unit for the type `std::chrono::minutes`.
\return "min"
*/
template<> inline std::string
duration_unit<std::chrono::minutes::period::num, std::chrono::minutes::period::den>() {
return "min";
}
/** \brief Specialization of the compact `std::string` form of the `std::chrono::duration` base time
unit for the type `std::chrono::hours`.
\return "h"
*/
template<> inline std::string
duration_unit<std::chrono::hours::period::num, std::chrono::hours::period::den>() {
return "h";
}
/** \brief Converts a `std::chrono::duration` to a `std::string` in compact form.
This is the compact form of convert::to_string(const std::chrono::duration<Rep, std::ratio<Num, Denom> >&).
\return a `std::string` of the form "n [SI prefix][base unit]". `n` is the number of ticks in the
`std::chrono::duration` converted to a `std::string`. [base unit] is a base time unit in compact
form (usually "s", the symbol of the SI second). [SI prefix symbol] is an SI prefix (such as "m" for
"milli") which is present if the base unit is seconds and the argument's period type is an SI ratio.
*/
template<typename Rep, std::intmax_t Num, std::intmax_t Denom>
inline std::string to_string(const std::chrono::duration<Rep, std::ratio<Num, Denom> >& d) {
auto str = std::to_string(d.count());
// Determine the reduced Num/Denom ratio
constexpr std::intmax_t num = std::ratio<Num, Denom>::num;
constexpr std::intmax_t den = std::ratio<Num, Denom>::den;
// Determine the base time unit (usually seconds, but may be minutes, hours, etc.)
auto unit_str = duration_unit<num, den>();
// Add the ratio prefix (e.g. "m") if the base unit is seconds and the ratio is not 1
if (unit_str == "s" && !(num == 1 && den == 1)) {
str += to_string(std::ratio<num, den>());
}
str += unit_str;
return str;
}
/*
template<> inline std::string to_string(const std::yocto& r) {
return "y";
}
template<> inline std::string to_string(const std::zepto& r) {
return "z";
}*/
/** \brief Converts the ratio `std::atto` to a `std::string` consisting of the symbol for the SI
prefix "atto".
\return "a"
*/
template<> inline std::string to_string(const std::atto& r) {
return "a";
}
/** \brief Converts the ratio `std::femto` to a `std::string` consisting of the symbol for the SI
prefix "femto".
\return "f"
*/
template<> inline std::string to_string(const std::femto& r) {
return "f";
}
/** \brief Converts the ratio `std::pico` to a `std::string` consisting of the symbol for the SI
prefix "pico".
\return "p"
*/
template<> inline std::string to_string(const std::pico& r) {
return "p";
}
/** \brief Converts the ratio `std::nano` to a `std::string` consisting of the symbol for the SI
prefix "nano".
\return "n"
*/
template<> inline std::string to_string(const std::nano& r) {
return "n";
}
/** \brief Converts the ratio `std::micro` to a `std::string` consisting of a representation of the
symbol for the SI prefix "micro".
\return "u", a substitute representation of the SI symbol for micro, which is based on the Greek letter
mu.
*/
template<> inline std::string to_string(const std::micro& r) {
return "u";
}
/** \brief Converts the ratio `std::milli` to a `std::string` consisting of the symbol for the SI
prefix "milli".
\return "m"
*/
template<> inline std::string to_string(const std::milli& r) {
return "m";
}
/** \brief Converts the ratio `std::centi` to a `std::string` consisting of the symbol for the SI
prefix "centi".
\return "c"
*/
template<> inline std::string to_string(const std::centi& r) {
return "c";
}
/** \brief Converts the ratio `std::deci` to a `std::string` consisting of the symbol for the SI
prefix "deci".
\return "d"
*/
template<> inline std::string to_string(const std::deci& r) {
return "d";
}
/** \brief Converts the ratio `std::deca` to a `std::string` consisting of the symbol for the SI
prefix "deca".
\return "da"
*/
template<> inline std::string to_string(const std::deca& r) {
return "da";
}
/** \brief Converts the ratio `std::hecto` to a `std::string` consisting of the symbol for the SI
prefix "hecto".
\return "h"
*/
template<> inline std::string to_string(const std::hecto& r) {
return "h";
}
/** \brief Converts the ratio `std::kilo` to a `std::string` consisting of the symbol for the SI
prefix "kilo".
\return "k"
*/
template<> inline std::string to_string(const std::kilo& r) {
return "k";
}
/** \brief Converts the ratio `std::mega` to a `std::string` consisting of the symbol for the SI
prefix "mega".
\return "M"
*/
template<> inline std::string to_string(const std::mega& r) {
return "M";
}
/** \brief Converts the ratio `std::giga` to a `std::string` consisting of the symbol for the SI
prefix "giga".
\return "G"
*/
template<> inline std::string to_string(const std::giga& r) {
return "G";
}
/** \brief Converts the ratio `std::tera` to a `std::string` consisting of the symbol for the SI
prefix "tera".
\return "T"
*/
template<> inline std::string to_string(const std::tera& r) {
return "T";
}
/** \brief Converts the ratio `std::peta` to a `std::string` consisting of the symbol for the SI
prefix "peta".
\return "P"
*/
template<> inline std::string to_string(const std::peta& r) {
return "P";
}
/** \brief Converts the ratio `std::exa` to a `std::string` consisting of the symbol for the SI
prefix "exa".
\return "E"
*/
template<> inline std::string to_string(const std::exa& r) {
return "E";
}
/*
template<> inline std::string to_string(const std::zetta& r) {
return "Z";
}
template<> inline std::string to_string(const std::yotta& r) {
return "Y";
}*/
/** \brief Inserts a `std::chrono::duration` in compact form into a `std::ostream`.
The argument is converted to a `std::string` using
to_string(const std::chrono::duration<Rep, std::ratio<Num, Denom> >&),
which is inserted into the `std::ostream`.
*/
template<typename Rep, std::intmax_t Num, std::intmax_t Denom>
std::ostream& operator<<(std::ostream& os, const std::chrono::duration<Rep, std::ratio<Num, Denom> >& d) {
return os << convert::compact::to_string<Rep, Num, Denom>(d);
}
} // end namespace compact
} // end namespace convert
#endif
For the std::chrono::duration
conversions, duration_unit()
is intended to be specialized by users if desired for specific time periods.
Here is a little demo program demonstrating the conversions and a specialization of duration_unit()
for a time period corresponding to a day:
#include <iostream>
#include <fstream>
#include <string>
#include "convert.h"
using namespace std::chrono_literals;
template<typename T>
void print(std::ostream& os, const T& t) {
os << '\'' << convert::to_string(t) << "\' or \'"
<< convert::compact::to_string(t) << "\' using to_string(),\t";
{
using convert::operator<<;
os << '\'' << t << "\' or \'";
}
{
using convert::compact::operator<<;
os << t << "\' using operator<<\n";
}
}
// User-defined duration unit strings
namespace convert {
template<> inline std::string
duration_unit<std::chrono::hours::period::num * 24, std::chrono::hours::period::den>() {
return "day";
}
namespace compact {
template<> inline std::string
duration_unit<std::chrono::hours::period::num * 24, std::chrono::hours::period::den>() {
return "d";
}
} // end namespace compact
} // end namespace convert
int main() {
std::ofstream ofs("demo.txt");
std::ostream& os = ofs; // or std::cout
os << "SI std::ratio conversions:\n";
print(os, std::atto());
print(os, std::femto());
print(os, std::pico());
print(os, std::nano());
print(os, std::micro());
print(os, std::milli());
print(os, std::centi());
print(os, std::deci());
print(os, std::deca());
print(os, std::hecto());
print(os, std::kilo());
print(os, std::mega());
print(os, std::giga());
print(os, std::tera());
print(os, std::peta());
print(os, std::exa());
os << '\n';
// Some non-SI ratios to test
auto r1 = std::ratio<60>();
auto r2 = std::ratio<1, 60>();
auto r3 = std::ratio<2, 120>(); // not in reduced form
auto r4 = std::ratio<1>();
os << "Other std::ratio conversions:\n";
print(os, r1);
print(os, r2);
print(os, r3);
print(os, r4);
os << '\n';
// Some durations to test
auto d1 = 4ns;
auto d2 = 1us;
auto d3 = 1.5ms;
auto d4 = -9.1s;
std::chrono::duration<int, std::ratio<120, 2> > d5(50); // period is equal to minutes
std::chrono::hours d6(2);
std::chrono::duration<std::intmax_t, std::exa> d7(7);
std::chrono::duration<double, std::ratio<24 * 60 * 60, 1> > d8(5.5); // days
os << "std::chrono::duration conversions:\n";
print(os, d1);
print(os, d2);
print(os, d3);
print(os, d4);
print(os, d5);
print(os, d6);
print(os, d7);
print(os, d8);
os << '\n';
return 0;
}
The output of the demo program is:
SI std::ratio conversions:
'atto' or 'a' using to_string(), 'atto' or 'a' using operator<<
'femto' or 'f' using to_string(), 'femto' or 'f' using operator<<
'pico' or 'p' using to_string(), 'pico' or 'p' using operator<<
'nano' or 'n' using to_string(), 'nano' or 'n' using operator<<
'micro' or 'u' using to_string(), 'micro' or 'u' using operator<<
'milli' or 'm' using to_string(), 'milli' or 'm' using operator<<
'centi' or 'c' using to_string(), 'centi' or 'c' using operator<<
'deci' or 'd' using to_string(), 'deci' or 'd' using operator<<
'deca' or 'da' using to_string(), 'deca' or 'da' using operator<<
'hecto' or 'h' using to_string(), 'hecto' or 'h' using operator<<
'kilo' or 'k' using to_string(), 'kilo' or 'k' using operator<<
'mega' or 'M' using to_string(), 'mega' or 'M' using operator<<
'giga' or 'G' using to_string(), 'giga' or 'G' using operator<<
'tera' or 'T' using to_string(), 'tera' or 'T' using operator<<
'peta' or 'P' using to_string(), 'peta' or 'P' using operator<<
'exa' or 'E' using to_string(), 'exa' or 'E' using operator<<
Other std::ratio conversions:
' 60' or ' 60' using to_string(), ' 60' or ' 60' using operator<<
' 1/60' or ' 1/60' using to_string(), ' 1/60' or ' 1/60' using operator<<
' 1/60' or ' 1/60' using to_string(), ' 1/60' or ' 1/60' using operator<<
' 1' or ' 1' using to_string(), ' 1' or ' 1' using operator<<
std::chrono::duration conversions:
'4 nanoseconds' or '4ns' using to_string(), '4 nanoseconds' or '4ns' using operator<<
'1 microsecond' or '1us' using to_string(), '1 microsecond' or '1us' using operator<<
'1.500000 milliseconds' or '1.500000ms' using to_string(), '1.500000 milliseconds' or '1.500000ms' using operator<<
'-9.100000 seconds' or '-9.100000s' using to_string(), '-9.100000 seconds' or '-9.100000s' using operator<<
'50 minutes' or '50min' using to_string(), '50 minutes' or '50min' using operator<<
'2 hours' or '2h' using to_string(), '2 hours' or '2h' using operator<<
'7 exaseconds' or '7Es' using to_string(), '7 exaseconds' or '7Es' using operator<<
'5.500000 days' or '5.500000d' using to_string(), '5.500000 days' or '5.500000d' using operator<<
I'm looking for suggestions to improve any aspect of the code. In particular, I'm looking for suggestions to improve my use of C++11 and C++14 (I'm compiling with Visual Studio 2017 but most of my C++ experience has been with a C++03 compiler). Also, some specific questions:
std::yocto
,std::zepto
,std::zetta
, andstd::yotta
are only defined ifstd::intmax_t
can represent the ratio, and indeed on my system they are not defined. Is there a way for me to detect when those types are defined on the system so that I can provide conversions for those types yet avoid compiler errors when they are not defined?- Eventually I plan to expand this library to support additional
std::basic_string
types, not juststd::string
. Are there any issues with this design which will cause problems when I add this support? - The SI symbol for "micro" is really "µ" but I'm using "u" for simplicity (and since the character type of
std::string
ischar
). Since I plan to expand this library for wider character types, is it possible to determine if "µ" can be represented properly for a given character type?