Проверка вещественных чисел на нормальность

Поскольку вычисления с вещественными числами допускают возникновение нештатных ситуаций, таких как выход за пределы области определения функции, получение математической бесконечности, потеря порядка и других, результат может содержать не число, а некое специальное значение, фактически описывающее характер проблемы. Все такие специальные значения имеют обобщающее название "не число" (Not A Number, NaN).

В предыдущих разделах книги мы их уже встречали. В частности, при выводе в журнал (см. раздел Числа в строки и обратно) они отображаются в виде текстовых меток (например, nan(ind), +inf и т.д.). Еще одной особенностью является то, что единственного значения NaN среди операндов какого-либо выражения достаточно, чтобы всё выражение перестало правильно рассчитываться и начало давать результат NaN. Исключение составляют только "не числа", представляющие плюc/минус бесконечности: если на них что-то разделить, получится ноль. Однако и здесь есть ожидаемое исключение: если разделить бесконечность на бесконечность, опять получим NaN.

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

В MQL5 есть 2 функции, которые позволяют проанализировать вещественное число на нормальность: MathIsValidNumber выдает простой ответ — да (true) или нет (false), а MathClassify производит более подробную категоризацию.

На физическом уровне все специальные значения кодируются в числе особым сочетанием битов, которое не используется для представления обычных чисел. Для типов double и float эти кодировки, разумеется, отличаются. Попробуем "заглянуть за кулисы" double (как более востребованного, чем float).

В разделе Вложенные шаблоны мы создали класс Converter, который позволял переключать представление за счет совмещения двух разных типов в объединении. Воспользуемся этим классом, чтобы изучить битовое устройство NaN.

Для удобства перенесем класс в отдельный заголовочный файл ConverterT.mqh. В тестовом скрипте MathInvalid.mq5 подключим этот mqh-файл и создадим экземпляр конвертера для связки типов double/ulong (порядок следования не важен, конвертер способен работать в обе стороны).

static Converter<ulong,doubleNaNs;

Сочетание битов в NaN стандартизовано, поэтому возьмем несколько общеупотребительных значений, представленных константами ulong, и посмотрим, как на них реагируют встроенные функции.

// основные NaN
#define NAN_INF_PLUS  0x7FF0000000000000
#define NAN_INF_MINUS 0xFFF0000000000000
#define NAN_QUIET     0x7FF8000000000000
#define NAN_IND_MINUS 0xFFF8000000000000
   
// примеры пользовательских NaN
#define NAN_QUIET_1   0x7FF8000000000001
#define NAN_QUIET_2   0x7FF8000000000002
   
static double pinf = NaNs[NAN_INF_PLUS];  // +infinity
static double ninf = NaNs[NAN_INF_MINUS]; // -infinity
static double qnan = NaNs[NAN_QUIET];     // quiet NaN
static double nind = NaNs[NAN_IND_MINUS]; // -nan(ind)
   
void OnStart()
{
   PRT(MathIsValidNumber(pinf));               // false
   PRT(EnumToString(MathClassify(pinf)));      // FP_INFINITE
   PRT(MathIsValidNumber(nind));               // false
   PRT(EnumToString(MathClassify(nind)));      // FP_NAN
   ...
}

Как и ожидалось, результаты совпали.

Давайте познакомимся с формальным описанием функций MathIsValidNumber и MathClassify, а затем продолжим тесты.

bool MathIsValidNumber(double value)

Функция проверяет корректность действительного числа. Параметр может быть типа double или float. Результат true означает правильное число, а false — "не число" (одна из разновидностей NaN).

ENUM_FP_CLASS MathClassify(double value)

Функция возвращает категорию вещественного числа (типа double или float) — одно из значений перечисления ENUM_FP_CLASS:

  • FP_NORMAL — нормальное число;
  • FP_SUBNORMAL — число меньше, чем минимально представимое в нормализованном виде (например, для типа double это значения меньше DBL_MIN, 2.2250738585072014e-308); потеря порядка (точности);
  • FP_ZERO — ноль (положительный или отрицательный);
  • FP_INFINITE — бесконечность (положительная или отрицательная);
  • FP_NAN — все прочие виды "не чисел" (подразделяются на семейства "тихих" и "сигнальных" NaN).

"Сигнальные" NaN отсутствуют в MQL5. Они используются в механизме исключений (exceptions), который позволяет перехватывать и реагировать на критические ошибки внутри программы. В MQL5 такого механизма нет, поэтому, если случается, например, деление на 0, MQL-программа просто завершает свою работу (экстренно выгружается с графика).

"Тихих" NaN может быть много, и вы можете их конструировать с помощью конвертера, чтобы дифференцированно обозначать и обрабатывать нестандартные состояния в своих вычислительных алгоритмах.

Выполним несколько вычислений в MathInvalid.mq5, чтобы наглядно увидеть, каким образом могут получиться числа различных категорий.

   // вычисления с double
   PRT(MathIsValidNumber(0));                      // true
   PRT(EnumToString(MathClassify(0)));             // FP_ZERO
   PRT(MathIsValidNumber(M_PI));                   // true
   PRT(EnumToString(MathClassify(M_PI)));          // FP_NORMAL
   PRT(DBL_MIN / 10);                              // 2.225073858507203e-309
   PRT(MathIsValidNumber(DBL_MIN / 10));           // true
   PRT(EnumToString(MathClassify(DBL_MIN / 10)));  // FP_SUBNORMAL
   PRT(MathSqrt(-1.0));                            // -nan(ind)
   PRT(MathIsValidNumber(MathSqrt(-1.0)));         // false
   PRT(EnumToString(MathClassify(MathSqrt(-1.0))));// FP_NAN
   PRT(MathLog(0));                                // -inf
   PRT(MathIsValidNumber(MathLog(0)));             // false
   PRT(EnumToString(MathClassify(MathLog(0))));    // FP_INFINITE
   
   // вычисления с float
   PRT(1.0f / FLT_MIN / FLT_MIN);                             // inf
   PRT(MathIsValidNumber(1.0f / FLT_MIN / FLT_MIN));          // false
   PRT(EnumToString(MathClassify(1.0f / FLT_MIN / FLT_MIN))); // FP_INFINITE

Мы можем использовать конвертер в обратную сторону: по значению double получать его битовое представление и тем самым детектировать "не числа":

   PrintFormat("%I64X"NaNs[MathSqrt(-1.0)]);      // FFF8000000000000
   PRT(NaNs[MathSqrt(-1.0)] == NAN_IND_MINUS);      // true, nind

Функция PrintFormat аналогична StringFormat, единственное отличие в том, что результат сразу выводится в журнал, а не в строку.

Наконец убедимся, что "не числа" всегда не равны:

   // NaN != NaN всегда true
   PRT(MathSqrt(-1.0) != MathSqrt(-1.0)); // true
   PRT(MathSqrt(-1.0) == MathSqrt(-1.0)); // false

Для получения NaN или бесконечности в MQL5 существует способ, основанный на приведении строк "nan" и "inf" к double.

double nan = (double)"nan";
double infinity = (double)"inf";