Changing the range scale of values of arbitrary numeric types

c++, range, templates

Solution

First I think that your `ratio` probably needs to be some floating point type and computed using floating-point division (possibly another mechanism would work too). Otherwise if you try for example to scale from `[0, 19]` to `[0, 20]` you'll wind up with an integral ratio of `1` and perform no scaling whatsoever!

Next, let's assume that things work fine for floating-point types. Now we'll just do all our math as `double`, but if the output type is integral we'd like to round to the closest output integer rather than truncating down. So we can use `is_integral` to force some rounding into place (note I don't have access to compile/test this right now):

TYPE operator()(const TYPE& v)
{
    double vv(static_cast<double>(v));
    vv -= fmin;                // offset according to input minimum
    vv *= ratio;               // apply the scaling factor
    vv += tmin;                // offset according to output minimum
    return static_cast<TYPE>(vv + (0.5 * is_integral<TYPE>::value));  // Round for integral types
}

Problem

I have a need to convert a collection of numbers from one range to another, while keeping the relative distribution of the values. For example, a vector containing randomly-generated floats could be scaled to fit into possible unsigned char values (0..255). Ignoring the type conversion, this would mean that whatever input was provided (e.g. -1.0 to 1.0) , all numbers would be scaled to 0.0 to 255.0 (or thereabouts). I have created a template class to perform this conversion, which can be applied to a collection using `std::transform`: ``` template <class TYPE> class scale_value { const TYPE fmin, tmin, ratio; public: TYPE operator()(const TYPE& v) { TYPE vv(v); vv += (TYPE(0) - fmin); // offset according to input minimum vv *= ratio; // apply the scaling factor vv -= (TYPE(0) - tmin); // offset according to output minimum return vv; } // constructor takes input min,max and output min,max scale_value(const TYPE& pfmin, const TYPE& pfmax, const TYPE& ptmin, const TYPE& ptmax) : fmin(pfmin), tmin(ptmin), ratio((ptmax-tmin)/(pfmax-fmin)) { } // some code removed for brevity }; ``` However, the above code only works correctly for real numbers (`float`, `double`, ...). Integers work when scaling up, but even then only by whole ratios: ``` float scale_test_float[] = {0.0, 0.5, 1.0, 1.5, 2.0}; int scale_test_int[] = {0, 5, 10, 15, 20}; // create up-scalers scale_value<float> scale_up_float(0.0, 2.0, 100.0, 200.0); scale_value<int> scale_up_int(0, 20, 100, 200); // create down-scalers scale_value<float> scale_down_float(100.0, 200.0, 0.0, 2.0); scale_value<int> scale_down_int(100, 200, 0, 20); std::transform(scale_test_float, scale_test_float+5, scale_test_float, scale_up_float); // scale_test_float -> 100.0, 125.0, 150.0, 175.0, 200.0 std::transform(scale_test_int, scale_test_int+5, scale_test_int, scale_up_int); // scale_test_int -> 100, 125, 150, 175, 200 std::transform(scale_test_float, scale_test_float+5, scale_test_float, scale_down_float); // scale_test_float -> 0.0, 0.5, 1.0, 1.5, 2.0 std::transform(scale_test_int, scale_test_int+5, scale_test_int, scale_down_int); // scale_test_int -> 0, 0, 0, 0, 0 : fails due to ratio being rounded to 0 ``` My current solution to this issue is to store everything internal to `scale_value` as a `double`, and use type conversion as needed: ``` TYPE operator()(const TYPE& v) { double vv(static_cast<double>(v)); vv += (0.0 - fmin); // offset according to input minimum vv *= ratio; // apply the scaling factor vv -= (0.0 - tmin); // offset according to output minimum return static_cast<TYPE>(vv); } ``` This works for most cases, albeit with some errors with integers, as the values are truncated rather than rounded. For example scaling `{0,5,10,15,20}` from `0..20` to `20..35` and then back gives `{0,4,9,14,20}`. So, my question is, is there a better method of doing this? In the case of scaling a collection of `float`s, the type-conversions seem rather redundant, whereas when scaling `int`s there are errors introduced due to truncation. As an aside, I was surprised not to spot something (at least, nothing obvious) in boost for this purpose. Maybe I missed it - the various maths libraries confuse me. Edit: I realize I could specialize `operator()` for specific types, however this would mean a lot of code-duplication, which defeats one of the useful parts of templates. Unless there is a method to, say, specialize once for all non-float types (short, int, uint, ...).

Original source