Что эквивалентно Math.Round () с MidpointRounding.AwayFromZero в Delphi? - PullRequest
13 голосов
/ 24 июня 2019

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

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

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

Выход: 2.13

В Delphi?

Ответы [ 2 ]

13 голосов
/ 24 июня 2019

Я полагаю, что функция 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 + модульных тестовых случаев , но я пока не считаю набор тестов вполне достаточным.)

12 голосов
/ 24 июня 2019

То, что вы ищете, это 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

...