Информация об изменениях

Сообщение Re[5]: Как изящнее реализовать банковское округление? от 17.03.2021 7:11

Изменено 18.03.2021 10:43 netch80

Re[5]: Как изящнее реализовать банковское округление?
Здравствуйте, Marty, Вы писали:

M>Может кто заметит, что я не протестил


Я бы на все roundHalf* добавил специфические для них пограничные случаи типа 23.49999, 23.50001. Это может быть полезно, если не на текущий алгоритм, то против регрессии при возможной переделке.

M> precisionFitTo(requestedPrecision + 1);

M> unum_t ldd = getLowestDecimalDigit();

Ну вот тут таки кажется, что ты теряешь вариант типа 23.51, 23.501, 23.5001... этот precisionFitTo, если просто обрубает хвост, то теряет важные цифры на таких пограничных значениях.
Причём это будет касаться не только roundHalfEven, но и, например, roundCeil для 23.01. Первым рывком ты урезаешь до 23.0 отсекая хвост, потом видишь 0 и останавливаешься на 23, когда должно быть 24.

Добавь тесты на значение типа 23.01, 23.51 и проверь. Для полного набора ещё включить 23.49, 23.99.

Имеет смысл переделать эту precisionFitTo с возвратом, например, bool-признака "было ли ненулевое в отсечённой части" (назову его sticky по традиции). А дальше по этому признаку, например:

bool sticky = precisionFitTo(requestedPrecision + 1);
unum_t ldd = getLowestDecimalDigit();
if (ldd == 0 && !sticky) {
  // Значение точное, округления не происходит, режим не имеет значения
  precisionFitTo(requestedPrecision);
  return *this;
}
// Обеспечить для roundHalf* режимов, чтобы ldd==5 значило точно случай 0.5.
// Остальные входные значения будут давать или меньше, или больше 5.
// После этой точки stickу не нужен. ldd не может быть равно 0 (устранено раньше).
if (sticky && ldd == 5) {
  ++ldd;
}
precisionShrinkTo( requestedPrecision ); // будем корректировать на 1 от этого значения

switch(roundingMethod)
{
  case RoundingMethod::roundDown: // roundFloor
  {
    if (sgn() < 0) {
      *this -= makeMinimalPrecisionOne();
    }
    break;
  }
  // ... с остальными direct разберёшься по аналогии ...
  case RoundingMethod::roundHalfToInf: // roundHalfTowardsPositiveInf
  {
    if (ldd>=5)
      incrementMantissa();
    break;
  }
  case RoundingMethod::roundHalfToZero:
  {
    if (ldd>5)
      incrementMantissa();
    break;
  }
  // ...
  case RoundingMethod::roundHalfToEven: // roundBankers, roundBanking
  if (ldd > 5 || (ldd==5 && abs() % 2 != 0)))
  {
    incrementMantissa();
  }
  break;
  // ... и так далее ...

// Задолбало выписывать каждый раз.
// TODO Явно можно удешевить.
inline void Decimal::incrementMantissa()
{
    if (sign() > 0)
      *this  += makeMinimalPrecisionOne();
    if (sign() < 0)
      *this  -= makeMinimalPrecisionOne();
}


Ещё очень полезно перемапить режимы округления на основании знака и после этого работать только с мантиссой, например:
roundDown, >0 => roundToZero
roundUp, >0 => roundToInf
roundDown, <0 => roundToInf
roundUp, >0 => roundToZero
...
roundHalfUp, >0 => roundHalfToInf
roundHalfDown, >0 => roundHalfToZero
roundHalfUp, <0 => roundHalfToZero
roundHalfDown, <0 => roundHalfToInf
...

а в switch оставить только уменьшенный набор и уже не смотреть на знак.
Это я в том же Berkeley Softfloat увидел.
Re[5]: Как изящнее реализовать банковское округление?
Здравствуйте, Marty, Вы писали:

M>Может кто заметит, что я не протестил


Я бы на все roundHalf* добавил специфические для них пограничные случаи типа 23.49999, 23.50001. Это может быть полезно, если не на текущий алгоритм, то против регрессии при возможной переделке.

M> precisionFitTo(requestedPrecision + 1);

M> unum_t ldd = getLowestDecimalDigit();

Ну вот тут таки кажется, что ты теряешь вариант типа 23.51, 23.501, 23.5001... этот precisionFitTo, если просто обрубает хвост, то теряет важные цифры на таких пограничных значениях.
Причём это будет касаться не только roundHalfEven, но и, например, roundCeil для 23.01. Первым рывком ты урезаешь до 23.0 отсекая хвост, потом видишь 0 и останавливаешься на 23, когда должно быть 24.

Добавь тесты на значение типа 23.01, 23.001, 23.51, 23.501 и проверь. Для полного набора ещё включить 23.49, 23.499, 23.99, 23.999.

Имеет смысл переделать эту precisionShrinkTo с возвратом, например, bool-признака "было ли ненулевое в отсечённой части" (назову его sticky по традиции). А дальше по этому признаку, например:

bool sticky = precisionShrinkTo(requestedPrecision + 1);
unum_t ldd = getLowestDecimalDigit();
// Обеспечить, чтобы ldd == 5 значило точно случай 0.5, а ldd == 0 - отсутствие необходимости округления.
// Фактически, это дословное round to prepare for shorter precision по описанию IBM.
if (sticky && (ldd == 0 || ldd == 5)) {
  ++ldd;
}
precisionShrinkTo(requestedPrecision); // будем корректировать на 1 от этого значения
if (ldd == 0) {
  // Значение точное, округления не происходит, режим не имеет значения
  return *this;
}
// После этой точки stickу не нужен. ldd не может быть равно 0 (устранено раньше).

switch(roundingMethod)
{
  case RoundingMethod::roundDown: // roundFloor
  {
    if (sgn() < 0) {
      *this -= makeMinimalPrecisionOne();
    }
    break;
  }
  // ... с остальными direct разберёшься по аналогии ...
  case RoundingMethod::roundHalfToInf: // roundHalfTowardsPositiveInf
  {
    if (ldd>=5)
      incrementMantissa();
    break;
  }
  case RoundingMethod::roundHalfToZero:
  {
    if (ldd>5)
      incrementMantissa();
    break;
  }
  // ...
  case RoundingMethod::roundHalfToEven: // roundBankers, roundBanking
  if (ldd > 5 || (ldd==5 && abs() % 2 != 0)))
  {
    incrementMantissa();
  }
  break;
  // ... и так далее ...

// Задолбало выписывать каждый раз.
// TODO Явно можно удешевить, если на знак вообще не смотреть, а работать с мантиссой.
inline void Decimal::incrementMantissa()
{
    if (sign() > 0)
      *this  += makeMinimalPrecisionOne();
    if (sign() < 0)
      *this  -= makeMinimalPrecisionOne();
}


Ещё очень полезно перемапить режимы округления на основании знака и после этого работать только с мантиссой, например:
roundDown, >0 => roundToZero
roundUp, >0 => roundToInf
roundDown, <0 => roundToInf
roundUp, >0 => roundToZero
...
roundHalfUp, >0 => roundHalfToInf
roundHalfDown, >0 => roundHalfToZero
roundHalfUp, <0 => roundHalfToZero
roundHalfDown, <0 => roundHalfToInf
...

а в switch оставить только уменьшенный набор и уже не смотреть на знак.
Это я в том же Berkeley Softfloat увидел.