2013-02-21 18 views
6

Мне нужно преобразовать набор чисел из одного диапазона в другой, сохраняя относительное распределение значений.Изменение шкалы диапазона значений произвольных числовых типов

Например, вектор, содержащий произвольно сгенерированные поплавки, можно масштабировать, чтобы поместиться в возможные значения unsigned char (0..255). Игнорируя преобразование типа, это означало бы, что любой вход был предоставлен (например, от -1.0 до 1.0), все числа будут масштабированы до 0.0 до 255.0 (или около того).

Я создал класс шаблона для выполнения этого преобразования, который может быть применен к коллекции с помощью 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 
}; 

Однако приведенный выше код работает корректно только для действительных чисел (float, double, .. .). Целые работать при масштабировании до, но даже тогда только по целые коэффициенты:

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 

Мое текущее решение этого вопроса хранить все внутренние для scale_value как double и использовать преобразование типов при необходимости:

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); 
} 

Это работает в большинстве случаев, хотя и с некоторыми ошибками с целыми числами, поскольку значения усекаются, а не округлены. Например, масштабирование {0,5,10,15,20} от 0..20 до 20..35, а затем назад дает {0,4,9,14,20}.

Итак, мой вопрос в том, есть ли лучший способ сделать это? В случае масштабирования коллекции float s преобразования типов кажутся довольно избыточными, тогда как при масштабировании int s возникают ошибки из-за усечения.

В стороне, я был удивлен, не заметив что-то (по крайней мере, ничего очевидного) в boost для этой цели. Возможно, я пропустил это - разные библиотеки математики меня путают.

Edit: Я понимаю, что я мог бы специализироваться operator() для конкретных типов, но это означало бы много кода дублирования, который побеждает один из полезных частей шаблонов. Если не существует способа, скажем, специализироваться один раз для всех неплавающих типов (short, int, uint, ...).

+0

Что вы хотите, чтобы ваш код делать, если, например, вы пытаетесь масштабировать целочисленный диапазон '[1, 3]' в целочисленный диапазон '[1, 2]'? Должен ли источник 2s масштабироваться до '1',' 2' или 50/50 разделить между двумя значениями результата? –

+0

Я ... не определился :) В моих целях это не имеет большого значения, что происходит - я думаю, что добавление в разделение 50/50 добавило бы много сложности, требуя отслеживания предыдущих значений. – icabod

+0

Спасибо за этот замечательный фрагмент кода. Я всегда выполнял масштабирование диапазона «вручную», всегда отлаживая много глупых ошибок, это избавит меня от многих головных болей. – Avio

ответ

3

Прежде всего, я думаю, что ваш ratio, вероятно, должен быть некоторым типом с плавающей точкой и вычисляться с использованием деления с плавающей запятой (возможно, будет работать и другой механизм). В противном случае, если вы попробуете, например, от шкалы от [0, 19] до [0, 20], вы завершите интегральное соотношение 1 и не выполняйте масштабирование вообще!

Далее, давайте предположим, что все работает нормально для типов с плавающей точкой. Теперь мы будем делать всю нашу математику как double, но если тип вывода является интегральным, мы бы хотели округлить до ближайшего выходного целого числа, а не усекать вниз. Таким образом, мы можем использовать is_integral, чтобы заставить некоторое округление на место (обратите внимание, я не имею доступа к компиляции/теста это прямо сейчас):

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 
} 
+0

+1: Мне нравится этот подход (хотя is_integral - это C++ 11 .. это также TR1?), Так как это означает отсутствие дублирования кода. В моем собственном ответе я попытался сделать что-то подобное для C++ 03, но в конечном итоге специализировался на всем классе, чтобы удалить приводы :) – icabod

+0

Хотя я закончил использовать [мой собственный] (http: // stackoverflow.com/a/15007297/218597), я собираюсь принять это, поскольку он достигает моей цели не специализироваться на каждом отдельном типе, и без C++ 11 мы можем использовать 'boost :: is_integral' чтобы получить тот же эффект. – icabod

1

Округление - это ваша ответственность, а не компьютер/компилятор.

В вашем операторе() вам необходимо предоставить «закругляющий бит» при умножении.

Я хотел бы попробовать, начиная с чем-то вроде:

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); 
} 

Вы должны определить ЗНАК (х) функцию, если ваш компилятор уже не предоставил.

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

Хороший подход для различных int, но когда TYPE уже является двойным или плавающим, не применимо ли нежелательное смещение +/- 0,5? – icabod

+0

@icabod: Ты прав. Он должен будет использовать специализированную специализацию для применения этой версии оператора() только к целым типам. –

2

По предложению @John Р. Штром, который будет работать для Целые, я пришел со следующим, который, кажется, работает только с необходимостью обеспечить две специализации класса (мое беспокойство было необходимости напишите специализацию для каждого типа). Однако это требует написания «признака» для каждого не целого типа.

Сначала я создать «черты» -style класс (обратите внимание, что в C++ 11 Я думаю, что это уже предусмотрено в станд :: is_floating_point, но сейчас я застрял с ванильным 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; }; 

Используя этот класс черты стиля, мы можем обеспечить базовую «целое число» реализацией scale_value класса:

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)) 
    { 
    } 
}; 

... и частичную специализацию для случаев, когда параметр TYPE имеет «черту» который говорит, что это какой-то поплавок:

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)) {} 
}; 

Основные различие между этими классами является то, что с внедрением в целочисленных данных в классах хранятся как double, и есть встроенное округление в соответствии с ответом Джона.

Если я решил, что мне нужно реализовать класс с фиксированной точкой, то, я думаю, мне нужно будет добавить это как еще один признак.

Смежные вопросы