Что эквивалентно Math.Round() с MidpointRounding.AwayFromZero в Delphi? - программирование
Подтвердить что ты не робот

Что эквивалентно Math.Round() с MidpointRounding.AwayFromZero в Delphi?

Как использовать С# аналог Math.Round с MidpointRounding.AwayFromZero в Delphi?

Что будет эквивалентно:

double d = 2.125;
Console.WriteLine(Math.Round(d, 2, MidpointRounding.AwayFromZero));

Выход: 2.13

В Delphi?

4b9b3361

Ответ 1

То, что вы ищете, это функция SimpleRoundTo в сочетании с SetRoundMode. Как сказано в документации:

SimpleRoundTo возвращает ближайшее значение, которое имеет указанную степень десять. В случае, если AValue находится точно в середине двух ближайших значений, которые имеют указанную степень десяти (выше и ниже), эта функция возвращает:

  • Значение в сторону плюс бесконечность, если AValue является положительным.

  • Значение в сторону минус бесконечность, если AValue отрицательно и режим округления FPU не установлен в rmUp

Обратите внимание, что вторым параметром функции является TRoundToRange который относится к показателю степени (степень 10), а не к числу дробных цифр в методе .NET Math.Round. Поэтому для округления до 2 знаков после запятой вы используете -2 в качестве округления до диапазона.

uses Math, RTTI;

var
  LRoundingMode: TRoundingMode;
begin
  for LRoundingMode := Low(TRoundingMode) to High(TRoundingMode) do
  begin
    SetRoundMode(LRoundingMode);
    Writeln(TRttiEnumerationType.GetName(LRoundingMode));
    Writeln(SimpleRoundTo(2.125, -2).ToString);
    Writeln(SimpleRoundTo(-2.125, -2).ToString);
  end;
end;

rmNearest

2,13

-2, 13

rmDown

2,13

-2, 13

rmUp

2,13

-2, 12

rmTruncate

2,13

-2, 13

Ответ 2

Я считаю, что функция Delphi RTL SimpleRoundTo делает это по существу, по крайней мере, если режим округления FPU является "правильным". Пожалуйста, внимательно ознакомьтесь с его документацией и реализацией, а затем решите, подходит ли она для ваших целей.

Но имейте в виду, что установка режима округления для одной операции округления, подобной этой, использует глобальное изменение для решения локальной проблемы. Это может вызвать проблемы (многопоточность, библиотеки и т.д.).

Бонусная болтовня: Если бы вопрос был о "регулярном" округлении (до целого числа), я думаю, что я попробовал такой подход, как

function RoundMidpAway(const X: Real): Integer;
begin
  Result := Trunc(X);
  if Abs(Frac(X)) >= 0.5 then
    Inc(Result, Sign(X));
end;

вместо.

Конечно, можно написать аналогичную функцию даже для общего случая n дробных цифр. (Но будьте осторожны, чтобы правильно обрабатывать крайние случаи, переполнения, проблемы с плавающей точкой и т.д.)

Обновление: я полагаю, что следующее делает трюк (и быстро):

function RoundMidpAway(const X: Real): Integer; overload;
begin
  Result := Trunc(X);
  if Abs(Frac(X)) >= 0.5 then
    Inc(Result, Sign(X));
end;

function RoundMidpAway(const X: Real; ADigit: integer): Real; overload;
const
  PowersOfTen: array[-10..10] of Real =
    (
      0.0000000001,
      0.000000001,
      0.00000001,
      0.0000001,
      0.000001,
      0.00001,
      0.0001,
      0.001,
      0.01,
      0.1,
      1,
      10,
      100,
      1000,
      10000,
      100000,
      1000000,
      10000000,
      100000000,
      1000000000,
      10000000000
    );
var
  MagnifiedValue: Real;
begin
  if not InRange(ADigit, Low(PowersOfTen), High(PowersOfTen)) then
    raise EInvalidArgument.Create('Invalid digit index.');
  MagnifiedValue := X * PowersOfTen[-ADigit];
  Result := RoundMidpAway(MagnifiedValue) * PowersOfTen[ADigit];
end;

Конечно, если вы будете использовать эту функцию в рабочем коде, вы также добавите не менее 50 тестовых случаев, которые проверяют ее правильность (для ежедневного запуска).

Обновление: я считаю, что следующая версия более стабильна:

function RoundMidpAway(const X: Real; ADigit: integer): Real; overload;
const
  FuzzFactor = 1000;
  DoubleResolution = 1E-15 * FuzzFactor;
  PowersOfTen: array[-10..10] of Real =
    (
      0.0000000001,
      0.000000001,
      0.00000001,
      0.0000001,
      0.000001,
      0.00001,
      0.0001,
      0.001,
      0.01,
      0.1,
      1,
      10,
      100,
      1000,
      10000,
      100000,
      1000000,
      10000000,
      100000000,
      1000000000,
      10000000000
    );
var
  MagnifiedValue: Real;
  TruncatedValue: Real;
begin

  if not InRange(ADigit, Low(PowersOfTen), High(PowersOfTen)) then
    raise EInvalidArgument.Create('Invalid digit index.');
  MagnifiedValue := X * PowersOfTen[-ADigit];

  TruncatedValue := Int(MagnifiedValue);
  if CompareValue(Abs(Frac(MagnifiedValue)), 0.5, DoubleResolution * PowersOfTen[-ADigit]) >= EqualsValue  then
    TruncatedValue := TruncatedValue + Sign(MagnifiedValue);

  Result := TruncatedValue * PowersOfTen[ADigit];

end;

но я не полностью проверил это. (В настоящее время он проходит тестовые примеры 900+, но я пока не считаю набор тестов вполне достаточным.)

Ответ 3

Вы можете установить режим округления FPU, но отклонение от нуля не является одним из доступных вариантов. Если у вас есть только положительные числа, вы можете использовать Up, но если вам нужны и отрицательные числа, вам понадобится один из ваших.

Я не смог найти его с помощью быстрого поиска, но эта функция должна помочь.

function RoundAwayFromZero(AValue: Double; ADigit: TRoundToRange = 0): Double;
begin
  Result := Sign(AValue) * Ceil(Abs(AValue) * Power(10, -ADigit)) / Power(10, -ADigit);
end;

Можно назвать это так:

ShowMessage(RoundAwayFromZero(2.125, -2).ToString);  // 2.13
ShowMessage(RoundAwayFromZero(-2.125, -2).ToString);  // -2.13

Обратите внимание, что второй параметр должен быть отрицательным для десятичных цифр или положительным, если вы хотите округлить, скажем, от 199 до 200. Я сделал это таким образом, чтобы он соответствовал существующим функциям, таким как SimpleRoundTo.

Возможно, это не самый оптимальный вариант, поскольку он может сохранить результат Power или полностью обойти его, если ADigit равен 0, но я надеюсь, что пока это не будет проблемой.

Альтернативная реализация может проверять знак числа, а затем режим округления "Вверх" или "Вниз" в зависимости от результата, но я не знаю, хорошая ли это идея, поскольку настройка режима округления применяется ко всему вашему приложению, так что вы Я должен восстановить его, как только функция будет выполнена, и, возможно, все еще будут возникать проблемы, когда вы начнете работать с потоками.