Change the scale of the range of values ​​of arbitrary numeric types

I need to convert a set of numbers from one range to another, while maintaining a relative distribution of values.

For example, a vector containing randomly generated floats can be scaled to fit into possible unsigned char values ​​(0..255). Ignoring type conversion, this would mean that no matter what input was provided (e.g. -1.0 to 1.0), all numbers will be scaled to 0.0 to 255.0 (or so).

I created a template class to perform this conversion, which can be applied to the 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, but even then only whole relations:

 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 problem is to save all the internal scale_value value as 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 in most cases, although with some integer errors, as the values ​​are truncated rather than rounded. For example, scaling {0,5,10,15,20} from 20..35 to 20..35 , and then back gives {0,4,9,14,20} .

So my question is: is there a better way to do this? In the case of scaling the float s collection, float conversions seem unnecessary, while truncating int errors appear.

As an aside, I was surprised not to notice something (at least nothing obvious) in boost for this purpose. Perhaps I skipped this - different math libraries confuse me.

Edit: I understand that I could specialize operator() for certain types, however, that would mean a lot of code duplication, which defeats one of the useful parts of templates. If there is no way, for example, to specialize once for all non-floating types (short, int, uint, ...).

+6
source share
3 answers

At first, I think that your ratio should probably be some type of floating point and computed using floating point division (another mechanism might work). Otherwise, if you try, for example, to scale from [0, 19] to [0, 20] , you end up with an integral relation of 1 and do not perform any scaling!

Next, suppose everything works fine for floating point types. Now we just do all our math as double , but if the output type is integral, we would like to round to the nearest output integer, rather than shorten it. Therefore, we can use is_integral to force rounding (note that I do not have compilation / testing access 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 } 
+3
source

Following @John R. Strohm's suggestion, which will work for integers, I came up with the following, which seems to work only with the need to provide two class specializations (my task was to write a specialization for each type). However, this requires a spelling for each non-integer type.

First I create a β€œdamn” class - style (note that in C ++ 11 I think this is already specified in std :: is_floating_point, but for now I’m stuck with C ++ vanilla):

 template <class NUMBER> struct number_is_float { static const bool val = false; }; template<> struct number_is_float<float> { static const bool val = true; }; template<> struct number_is_float<double> { static const bool val = true; }; template<> struct number_is_float<long double> { static const bool val = true; }; 

Using this class of style styles, we can provide a basic implementation of the integer of the scale_value class:

 template <class TYPE, bool IS_FLOAT=number_is_float<TYPE>::val> class scale_value { private: const double fmin, tmin, ratio; public: TYPE operator()(const TYPE& v) { double vv(static_cast<double>(v)); vv += (0.0 - fmin); vv *= ratio; vv += 0.5 * ((static_cast<double>(v) >= 0.0) ? 1.0 : -1.0); vv -= (0.0 - tmin); return static_cast<TYPE>(vv); } scale_value(const TYPE& pfmin, const TYPE& pfmax, const TYPE& ptmin, const TYPE& ptmax) : fmin(static_cast<double>(pfmin)) , tmin(static_cast<double>(ptmin)) , ratio((static_cast<double>(ptmax)-tmin)/(static_cast<double>(pfmax)-fmin)) { } }; 

... and partial specialization for cases when the TYPE parameter has a β€œsign”, which speaks of it as a swim:

 template <class TYPE> class scale_value<TYPE, true> { private: const TYPE fmin, tmin, ratio; public: TYPE operator()(const TYPE& v) { TYPE vv(v); vv += (TYPE(0.0) - fmin); vv *= ratio; vv -= (TYPE(0.0) - tmin); return vv; } scale_value(const TYPE& pfmin, const TYPE& pfmax, const TYPE& ptmin, const TYPE& ptmax) : fmin(pfmin), tmin(ptmin), ratio((ptmax-tmin)/(pfmax-fmin)) {} }; 

The main differences between these classes are that when implementing an integer, the data in the classes is stored as double , and at John’s request there is a built-in rounding.

If I decide that I need to implement a class with a fixed point, then I probably need to add this as another sign.

+2
source

Rounding is your responsibility, not a computer / compiler.

In your statement (), you need to provide a "rounding bit" in the multiplication.

I would try to start with something like:

 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 += SIGN(static_cast<double>(v))*0.5; vv -= (0.0 - tmin); // offset according to output minimum return static_cast<TYPE>(vv); } 

You will need to define the SIGN (x) function if your compiler has not provided it yet.

 double SIGN(const double x) { return (x >= 0) ? 1.0 : -1.0; } 
+1
source

All Articles