Skip to content

More principled handling of float<->string #1785

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ addons:
- libavformat-dev
- libswscale-dev
- libavutil-dev
- locales
# - qtbase5-dev # FIXME: enable Qt5 on Linux
# - bzip2
# - libtinyxml-dev
Expand Down
2 changes: 1 addition & 1 deletion src/fits.imageio/fitsinput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ FitsInput::add_to_spec (const std::string &keyname, const std::string &value)
// converting string to float or integer
bool isNumSign = (value[0] == '+' || value[0] == '-' || value[0] == '.');
if (isdigit (value[0]) || isNumSign) {
float val = atof (value.c_str ());
float val = Strutil::stof (value);
if (val == (int)val)
m_spec.attribute (keyname, (int)val);
else
Expand Down
5 changes: 3 additions & 2 deletions src/include/OpenImageIO/optparser.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#define OPENIMAGEIO_OPTPARSER_H

#include <string>
#include <OpenImageIO/strutil.h>

OIIO_NAMESPACE_BEGIN

Expand Down Expand Up @@ -67,9 +68,9 @@ optparse1 (C &system, const std::string &opt)
char v = value.size() ? value[0] : ' ';
if ((v >= '0' && v <= '9') || v == '+' || v == '-') { // numeric
if (strchr (value.c_str(), '.')) // float
return system.attribute (name.c_str(), (float)atof(value.c_str()));
return system.attribute (name, Strutil::stof(value));
else // int
return system.attribute (name.c_str(), (int)atoi(value.c_str()));
return system.attribute (name, Strutil::stoi(value));
}
// otherwise treat it as a string

Expand Down
148 changes: 121 additions & 27 deletions src/include/OpenImageIO/strutil.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,35 +97,54 @@ void OIIO_API sync_output (std::ostream &file, string_view str);
///
/// Uses the tinyformat library underneath, so it's fully type-safe, and
/// works with any types that understand stream output via '<<'.
/// The formatting of the string will always use the classic "C" locale
/// conventions (in particular, '.' as decimal separator for float values).
template<typename... Args>
inline std::string format (string_view fmt, const Args&... args)
{
return tinyformat::format (fmt.c_str(), args...);
}

/// A version of Strutil::format() that uses an explicit locale.
template<typename... Args>
inline std::string format (const std::locale& loc, string_view fmt,
const Args&... args)
{
return tinyformat::format (loc, fmt.c_str(), args...);
}


/// Output formatted string to stdout, type-safe, and threads can't clobber
/// one another.
/// one another. This will force the classic "C" locale.
template<typename... Args>
inline void printf (string_view fmt, const Args&... args)
{
sync_output (stdout, format(fmt, args...));
}

/// Output to stdout using an explicit locale. If you want it to exactly
/// match the locale of std::cout, std::cout.getloc() as the locale.
template<typename... Args>
inline void printf (const std::locale& loc, string_view fmt, const Args&... args)
{
sync_output (stdout, format(loc, fmt, args...));
}

/// Output formatted string to an open FILE*, type-safe, and threads can't
/// clobber one another.
/// clobber one another. This will force classic "C" locale conventions.
template<typename... Args>
inline void fprintf (FILE *file, string_view fmt, const Args&... args)
{
sync_output (file, format(fmt, args...));
}

/// Output formatted string to an open ostream, type-safe, and threads can't
/// clobber one another.
/// clobber one another. This will honor the existing locale conventions of
/// the ostream.
template<typename... Args>
inline void fprintf (std::ostream &file, string_view fmt, const Args&... args)
{
sync_output (file, format(fmt, args...));
sync_output (file, format(file.getloc(), fmt, args...));
}


Expand Down Expand Up @@ -254,50 +273,125 @@ std::string OIIO_API repeat (string_view str, int n);
std::string OIIO_API replace (string_view str, string_view pattern,
string_view replacement, bool global=false);

// Helper template to test if a string is a generic type
template<typename T>
inline bool string_is (string_view /*s*/) {
return false; // Generic: assume there is an explicit specialization

/// strtod/strtof equivalents that are "locale-independent", always using
/// '.' as the decimal separator. This should be preferred for I/O and other
/// situations where you want the same standard formatting regardless of
/// locale.
float OIIO_API strtof (const char *nptr, char **endptr = nullptr);
double OIIO_API strtod (const char *nptr, char **endptr = nullptr);

/// strtod/strtof equivalents that take an explicit locale. This may be
/// useful for UI or I/O that you specifically want to use the conventions
/// of a localized country.
double OIIO_API strtod (const char *nptr, char **endptr, const std::locale& loc);
float OIIO_API strtof (const char *nptr, char **endptr, const std::locale& loc);
#define OIIO_STRUTIL_HAS_STRTOF 1 /* be able to test this */



// stoi() returns the int conversion of text from several string types.
// No exceptions or errors -- parsing errors just return 0.
OIIO_API int stoi (const char* s, size_t* pos=0, int base=10);
OIIO_API int stoi (const std::string& s, size_t* pos=0, int base=10);
OIIO_API int stoi (string_view s, size_t* pos=0, int base=10);
// N.B. For users of ustring, there's a stoi(ustring) defined in ustring.h.


// stoul() returns the unsigned int conversion of text from several string
// types. No exceptions or errors -- parsing errors just return 0.
OIIO_API unsigned int stoul (const char* s, size_t* pos=0, int base=10);
OIIO_API unsigned int stoul (const std::string& s, size_t* pos=0, int base=10);
OIIO_API unsigned int stoul (string_view s, size_t* pos=0, int base=10);
// N.B. For users of ustring, there's a stoi(ustring) defined in ustring.h.


/// stof() returns the float conversion of text from several string types.
/// No exceptions or errors -- parsing errors just return 0.0. These always
/// use '.' for the decimal mark (versus atof and std::strtof, which are
/// locale-dependent).
OIIO_API float stof (const std::string& s, size_t* pos=0);
OIIO_API float stof (const char* s, size_t* pos=0);
OIIO_API float stof (string_view s, size_t* pos=0);
// N.B. For users of ustring, there's a stof(ustring) defined in ustring.h.

// stof() version that takes an explicit locale (for example, if you pass a
// default-constructed std::locale, it will use the current native locale's
// decimal conventions).
OIIO_API float stof (const std::string& s, const std::locale& loc, size_t* pos=0);
OIIO_API float stof (const char* s, const std::locale& loc, size_t* pos=0);
OIIO_API float stof (string_view s, const std::locale& loc, size_t* pos=0);



/// Return true if the string is exactly (other than leading whitespace)
/// a valid int.
inline bool string_is_int (string_view s) {
size_t pos;
Strutil::stoi (s, &pos);
return pos && pos >= s.size(); // consumed the whole string
}
// Special case for int
template <> inline bool string_is<int> (string_view s) {
if (s.empty())
return false;
char *endptr = 0;
strtol (s.data(), &endptr, 10);
return (s.data() + s.size() == endptr);

/// Return true if the string is exactly (other than leading whitespace)
/// a valid float. This operations in a locale-independent manner, i.e.,
/// it assumes '.' as the decimal mark.
inline bool string_is_float (string_view s) {
size_t pos;
Strutil::stof (s, &pos);
return pos && pos >= s.size(); // consumed the whole string
}
// Special case for float
template <> inline bool string_is<float> (string_view s) {
if (s.empty())
return false;
char *endptr = 0;
strtod (s.data(), &endptr);
return (s.data() + s.size() == endptr);

/// Return true if the string is exactly (other than leading whitespace)
/// a valid float. This operations uses an explicit locale.
inline bool string_is_float (string_view s, const std::locale& loc) {
size_t pos;
Strutil::stof (s, loc, &pos);
return pos && pos >= s.size(); // consumed the whole string
}



// Helper template to convert from generic type to string
// Helper template to convert from generic type to string (using default
// locale). Used when you want stoX but you're in a template.
template<typename T>
inline T from_string (string_view s) {
return T(s); // Generic: assume there is an explicit converter
}
// Special case for int
template<> inline int from_string<int> (string_view s) {
return s.size() ? strtol (s.c_str(), NULL, 10) : 0;
return Strutil::stoi(s);
}
// Special case for uint
template<> inline unsigned int from_string<unsigned int> (string_view s) {
return s.size() ? strtoul (s.c_str(), NULL, 10) : (unsigned int)0;
return Strutil::stoul(s);
}
// Special case for float
// Special case for float -- note that by using Strutil::strtof, this
// always treats '.' as the decimal mark.
template<> inline float from_string<float> (string_view s) {
return s.size() ? (float)strtod (s.c_str(), NULL) : 0.0f;
return Strutil::stof(s);
}



// Helper template to test if a string is a generic type. Used instead of
// string_is_X, but when you're inside templated code.
template<typename T>
inline bool string_is (string_view /*s*/) {
return false; // Generic: assume there is an explicit specialization
}
// Special case for int
template <> inline bool string_is<int> (string_view s) {
return string_is_int (s);
}
// Special case for float. Note that by using Strutil::stof, this always
// treats '.' as the decimal character.
template <> inline bool string_is<float> (string_view s) {
return string_is_float (s);
}




/// Given a string containing values separated by a comma (or optionally
/// another separator), extract the individual values, placing them into
/// vals[] which is presumed to already contain defaults. If only a single
Expand Down
32 changes: 31 additions & 1 deletion src/include/OpenImageIO/tinyformat.h
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ template<typename T>
inline void formatTruncated(std::ostream& out, const T& value, int ntrunc)
{
std::ostringstream tmp;
tmp.imbue (out.getloc());
tmp << value;
std::string result = tmp.str();
out.write(result.c_str(), (std::min)(ntrunc, static_cast<int>(result.size())));
Expand Down Expand Up @@ -800,6 +801,7 @@ inline void formatImpl(std::ostream& out, const char* fmt,
// it crudely by formatting into a temporary string stream and
// munging the resulting string.
std::ostringstream tmpStream;
tmpStream.imbue (out.getloc());
tmpStream.copyfmt(out);
tmpStream.setf(std::ios::showpos);
arg.format(tmpStream, fmt, fmtEnd, ntrunc);
Expand Down Expand Up @@ -931,6 +933,7 @@ TINYFORMAT_FOREACH_ARGNUM(TINYFORMAT_MAKE_MAKEFORMATLIST)
#endif

/// Format list of arguments to the stream according to the given format string.
/// This honors the stream's existing locale conventions.
///
/// The name vformat() is chosen for the semantic similarity to vprintf(): the
/// list of format arguments is held in a single function argument.
Expand All @@ -943,23 +946,40 @@ inline void vformat(std::ostream& out, const char* fmt, FormatListRef list)
#ifdef TINYFORMAT_USE_VARIADIC_TEMPLATES

/// Format list of arguments to the stream according to given format string.
/// This honors the stream's existing locale conventions.
template<typename... Args>
void format(std::ostream& out, const char* fmt, const Args&... args)
{
vformat(out, fmt, makeFormatList(args...));
}

/// Format list of arguments according to the given format string and return
/// the result as a string.
/// the result as a string, using classic "C" locale conventions (e.g.,
/// using '.' as decimal mark).
template<typename... Args>
std::string format(const char* fmt, const Args&... args)
{
std::ostringstream oss;
oss.imbue (std::locale::classic()); // force "C" locale with '.' decimal
format(oss, fmt, args...);
return oss.str();
}

/// Format list of arguments according to the given format string and return
/// the result as a string, using an explicit locale. Passing loc as a
/// default-constructed std::locale will result in adhering to the current
/// "native" locale set by the OS.
template<typename... Args>
std::string format(const std::locale& loc, const char* fmt, const Args&... args)
{
std::ostringstream oss;
oss.imbue (loc);
format(oss, fmt, args...);
return oss.str();
}

/// Format list of arguments to std::cout, according to the given format string
/// This honors std::out's existing locale conventions.
template<typename... Args>
void printf(const char* fmt, const Args&... args)
{
Expand Down Expand Up @@ -1011,6 +1031,16 @@ template<TINYFORMAT_ARGTYPES(n)> \
std::string format(const char* fmt, TINYFORMAT_VARARGS(n)) \
{ \
std::ostringstream oss; \
oss.imbue (std::locale::classic()); \
format(oss, fmt, TINYFORMAT_PASSARGS(n)); \
return oss.str(); \
} \
\
template<TINYFORMAT_ARGTYPES(n)> \
std::string format(const std::locale& loc, const char* fmt, TINYFORMAT_VARARGS(n)) \
{ \
std::ostringstream oss; \
oss.imbue (loc); \
format(oss, fmt, TINYFORMAT_PASSARGS(n)); \
return oss.str(); \
} \
Expand Down
20 changes: 20 additions & 0 deletions src/include/OpenImageIO/ustring.h
Original file line number Diff line number Diff line change
Expand Up @@ -626,12 +626,22 @@ class OIIO_API ustring {
/// something like:
/// ustring s = ustring::format ("blah %d %g", (int)foo, (float)bar);
/// The argument list is fully typesafe.
/// The formatting of the string will always use the classic "C" locale
/// conventions (in particular, '.' as decimal separator for float values).
template<typename... Args>
static ustring format (string_view fmt, const Args&... args)
{
return ustring (Strutil::format (fmt, args...));
}

/// ustring formatting with an explicit locale.
template<typename... Args>
static ustring format (const std::locale& loc, string_view fmt,
const Args&... args)
{
return ustring (Strutil::format (loc, fmt, args...));
}

/// Generic stream output of a ustring.
///
friend std::ostream & operator<< (std::ostream &out, const ustring &str) {
Expand Down Expand Up @@ -751,6 +761,16 @@ inline bool iequals (const std::string &a, ustring b) {
}



// add ustring variants of stoi and stof from OpenImageIO/strutil.h
namespace Strutil {
inline int stoi (ustring s) { return Strutil::stoi (s.c_str()); }
inline int stof (ustring s) { return Strutil::stof (s.c_str()); }
inline int stof (ustring s, const std::locale& loc) {
return Strutil::stof (s.c_str(), loc);
}
} // end namespace Strutil

OIIO_NAMESPACE_END

#endif // OPENIMAGEIO_USTRING_H
9 changes: 3 additions & 6 deletions src/iv/imageviewer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,9 @@ ImageViewer::ImageViewer ()
{
readSettings (false);

const char *gamenv = getenv ("GAMMA");
if (gamenv) {
float g = atof (gamenv);
if (g >= 0.1 && g <= 5)
m_default_gamma = g;
}
float gam = Strutil::stof (Sysutil::getenv ("GAMMA"));
if (gam >= 0.1 && gam <= 5)
m_default_gamma = gam;
// FIXME -- would be nice to have a more nuanced approach to display
// color space, in particular knowing whether the display is sRGB.
// Also, some time in the future we may want a real 3D LUT for
Expand Down
1 change: 1 addition & 0 deletions src/libOpenImageIO/formatspec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,7 @@ spec_to_xml (const ImageSpec &spec, ImageSpec::SerialVerbose verbose)
}

std::ostringstream result;
result.imbue (std::locale::classic()); // force "C" locale with '.' decimal
doc.print (result, "");
return result.str();
}
Expand Down
Loading