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, ...).
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
}
Following @John R. Strohm's suggestion, which would work for Integers, I have come up with the following, which seems to work with only a need to provide two specializations of the class (my concern was having to write a specialization for every type). It does require writing a "trait" for each non-whole type, however.
First I create a "traits"-style class (note that in C++11 I think this is already provided in std::is_floating_point, but for now I'm stuck with vanilla C++):
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 traits-style class, we can provide a basic "whole number" implementation 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 a partial specialization for cases where the TYPE
parameter has a "trait" that says it's a float of some kind:
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 is that with the whole-number implementation, the data in the classes is stored as a double
, and there is built-in rounding as per John's answer.
If I decided I needed to implement a fixed-point class, then I guess I would need to add this as another trait.
Rounding is your responsibility, not the computer's/compiler's.
In your operator(), you need to provide a "rounding bit" in the multiplication.
I'd try starting 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'll have to define a SIGN(x) function, if your compiler doesn't already provide one.
double SIGN(const double x) {
return (x >= 0) ? 1.0 : -1.0;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With