Seltsames Float-Rundungsverhalten mit printf

7

Ich habe einige Antworten auf dieser Seite gelesen und fand die printfRundung wünschenswert.

Als ich es jedoch in der Praxis verwendete, führte mich ein subtiler Fehler zu folgendem Verhalten:

$ echo 197.5 | xargs printf '%.0f'
198
$ echo 196.5 | xargs printf '%.0f'
196
$ echo 195.5 | xargs printf '%.0f'
196

Beachten Sie, dass Rundung 196.5wird 196.

Ich weiß, dass dies ein subtiler Gleitkomma-Fehler sein kann (aber das ist keine sehr große Zahl, oder?), Kann also jemand etwas Licht darauf werfen?

Eine Problemumgehung hierfür ist ebenfalls sehr zu begrüßen (da ich versuche, dies jetzt zum Laufen zu bringen).

Hai Zhang
quelle
Wo sehen Sie einen Fehler? 196 ist eine durchaus sinnvolle Rundung von 196,5 auf 0 Stellen nach dem Dezimalpunkt und das Gleiche für 198 für 197,5. Fragen Sie sich, warum es manchmal auf- und abrundet? Das ist normal, da dies rundet und nicht abschneidet.
Gilles 'SO - hör auf böse zu sein'
PS Lesen Sie, was jeder Programmierer über Gleitkomma-Arithmetik wissen sollte .
Gilles 'SO - hör auf böse zu sein'
@ Gilles Nun, ich meine, es gibt einen Fehler in meinem eigenen Skript, der sich auf Gleitkomma-Arithmetik bezieht, weil es sich nicht so verhält, wie es das reale Szenario erfordert, und ich weiß, dass dies natürlich kein Fehler in IEEE 754 ist ...
Hai Zhang

Antworten:

15

Es ist wie erwartet, es ist "Round to Even" oder "Banker's Rounding".

Eine verwandte Site-Antwort erklärt es.

Das Problem, das eine solche Regel zu lösen versucht, ist das (für Zahlen mit einer Dezimalstelle),

  • x.1 bis x.4 werden abgerundet.
  • x.6 bis x.9 werden aufgerundet.

Das sind 4 runter und 4 rauf.
Um die Rundung im Gleichgewicht zu halten, müssen wir die x.5 runden

  • ein Mal hoch und das nächste runter .

Dies geschieht nach der Regel: «Auf die nächste gerade Zahl runden».

In Code:

LC_NUMERIC=C printf '%.0f ' "$value"
echo "$value" | awk 'printf( "%s", $1)'


Optionen:

Insgesamt gibt es vier Möglichkeiten, eine Zahl zu runden:

  1. Die bereits erläuterte Banker-Regel.
  2. Runde in Richtung + unendlich. Aufrunden (für positive Zahlen)
  3. Rund gegen unendlich. Abrunden (für positive Zahlen)
  4. Runde gegen Null. Entfernen Sie die Dezimalstellen (entweder positiv oder negativ).

Oben

Wenn Sie "Aufrunden (in Richtung +infinite)" benötigen , können Sie awk verwenden:

value=195.5

echo "$value" | awk '{ printf("%d", $1 + 0.5) }'
echo "scale=0; ($value+0.5)/1" | bc

Nieder

Wenn Sie "Abrunden (Richtung -infinite)" benötigen , können Sie Folgendes verwenden:

value=195.5

echo "$value" | awk '{ printf("%d", $1 - 0.5) }'
echo "scale=0; ($value-0.5)/1" | bc

Dezimalstellen kürzen.

Entfernen der Dezimalstellen (alles nach dem Punkt).
Wir könnten die Shell auch direkt verwenden (funktioniert bei den meisten Shells - ist POSIX):

value="127.54"    ### Works also for negative numbers.

echo "${value%%.*}"
echo "$value"| awk '{printf ("%d",$0)}'
echo "scale=0; ($value)/1" | bc

Gemeinschaft
quelle
4

Es ist kein Fehler, es ist beabsichtigt.
Es macht eine Art Runde zum nächsten (dazu später mehr).
Mit genau können .5wir so oder so runden. In der Schule wurde dir wahrscheinlich gesagt, du sollst abrunden, aber warum? Weil Sie dann keine weiteren Ziffern mehr untersuchen müssen, zB 3.51 auf 4 aufrunden; 3.5 könnte ätherisch sein, aber wenn wir nur die erste Ziffer betrachten und .5 aufrunden, dann machen wir es immer richtig.

Wenn wir uns jedoch den Satz von zweistelligen Dezimalstellen ansehen: 0,00 0,01, 0,02, 0,03… 0,98, 0,99, werden wir sehen, dass es 100 Werte gibt, 1 eine ganze Zahl ist, 49 aufgerundet werden müssen, 49 abgerundet werden müssen 1 (0,50) könnte ätherisch gehen. Wenn wir immer aufrunden, erhalten wir im Durchschnitt Zahlen, die 0,01 zu groß sind.

Wenn wir den Bereich auf 0 → 9,99 erweitern, haben wir 9 zusätzliche Werte für diese Aufrundung. Dadurch ist unser Durchschnitt etwas größer als erwartet. Ein Versuch, dies zu beheben, ist also: 0,5 Runden in Richtung gerade. Die Hälfte der Zeit, die es aufrundet, die Hälfte der Zeit, die es abrundet.

Dies ändert die Vorspannung von aufwärts zu gerade. In den meisten Fällen ist dies besser.

Strg-Alt-Delor
quelle
1

Das vorübergehende Ändern der Rundungsmodi ist nicht ungewöhnlich und es ist möglich, bin/printfdass Sie die Quellen ändern müssen, obwohl dies per se nicht der Fall ist.

Sie benötigen die Quellen der Coreutils. Ich habe die neueste Version verwendet, die heute verfügbar ist: http://ftp.gnu.org/gnu/coreutils/coreutils-8.24.tar.xz .

Entpacken Sie mit in ein Verzeichnis Ihrer Wahl

tar xJfv coreutils-8.24.tar.xz

Wechseln Sie in das Quellverzeichnis

cd coreutils-8.24

Laden Sie die Datei src/printf.cin den Editor Ihrer Wahl und tauschen Sie die gesamte mainFunktion mit der folgenden Funktion aus, einschließlich der beiden Präprozessoranweisungen, um die Headerdateien math.hund einzuschließen fenv.h. Die Hauptfunktion befindet sich am Ende und beginnt int main...und endet am Ende der Datei mit der schließenden Klammer}

#include <math.h>
#include <fenv.h>
int
main (int argc, char **argv)
{
  char *format;
  char *rounding_env;
  int args_used;
  int rounding_mode;

  initialize_main (&argc, &argv);
  set_program_name (argv[0]);
  setlocale (LC_ALL, "");
  bindtextdomain (PACKAGE, LOCALEDIR);
  textdomain (PACKAGE);

  atexit (close_stdout);

  exit_status = EXIT_SUCCESS;

  posixly_correct = (getenv ("POSIXLY_CORRECT") != NULL);
  // accept rounding modes from an environment variable
  if ((rounding_env = getenv ("BIN_PRINTF_ROUNDING_MODE")) != NULL)
    {
      rounding_mode = atoi(rounding_env);
      switch (rounding_mode)
        {
        case 0:
          if (fesetround(FE_TOWARDZERO) != 0)
            {
              error (0, 0, _("setting rounding mode to roundTowardZero failed"));
              return EXIT_FAILURE;
            }
          break;
       case 1:
          if (fesetround(FE_TONEAREST) != 0)
            {
              error (0, 0, _("setting rounding mode to roundTiesToEven failed"));
              return EXIT_FAILURE;
            }
          break;
       case 2:
          if (fesetround(FE_UPWARD) != 0)
            {
              error (0, 0, _("setting rounding mode to roundTowardPositive failed"));
              return EXIT_FAILURE;
            }
          break;
       case 3:
          if (fesetround(FE_DOWNWARD) != 0)
            {
              error (0, 0, _("setting rounding mode to roundTowardNegative failed"));
              return EXIT_FAILURE;
            }
          break;
       default:
         error (0, 0, _("setting rounding mode failed for unknown reason"));
         return EXIT_FAILURE;
      }
    }
  /* We directly parse options, rather than use parse_long_options, in
     order to avoid accepting abbreviations.  */
  if (argc == 2)
    {
      if (STREQ (argv[1], "--help"))
        usage (EXIT_SUCCESS);

      if (STREQ (argv[1], "--version"))
        {
          version_etc (stdout, PROGRAM_NAME, PACKAGE_NAME, Version, AUTHORS,
                       (char *) NULL);
          return EXIT_SUCCESS;
        }
    }

  /* The above handles --help and --version.
     Since there is no other invocation of getopt, handle '--' here.  */
  if (1 < argc && STREQ (argv[1], "--"))
    {
      --argc;
      ++argv;
    }

  if (argc <= 1)
    {
      error (0, 0, _("missing operand"));
      usage (EXIT_FAILURE);
    }

  format = argv[1];
  argc -= 2;
  argv += 2;

  do
    {
      args_used = print_formatted (format, argc, argv);
      argc -= args_used;
      argv += args_used;
    }
  while (args_used > 0 && argc > 0);

  if (argc > 0)
    error (0, 0,
           _("warning: ignoring excess arguments, starting with %s"),
           quote (argv[0]));

  return exit_status;
}

Führen Sie ./configurewie folgt aus

LIBS=-lm ./configure --program-suffix=-own

Das Suffix wird -ownbei jedem Unterprogramm eingefügt (es gibt viele), nur für den Fall, dass Sie alle installieren möchten und nicht sicher sind, ob sie zum Rest des Systems passen. Die Coreutils werden nicht ohne Grund als Core Utils bezeichnet!

Aber das Wichtigste ist das LIBS=-lmvor der Linie. Wir brauchen die mathematische Bibliothek und dieser Befehl weist Sie ./configurean, sie der Liste der benötigten Bibliotheken hinzuzufügen.

Führen Sie make aus

make

Wenn Sie ein Multicore- / Multiprozessorsystem haben, versuchen Sie es

make -j4

Dabei sollte die Zahl (hier "4") die Anzahl der Kerne darstellen, die Sie für diesen Job bereit haben.

Wenn alles gut gegangen ist, haben Sie den neuen printfInt src/printf. Versuch es:

BIN_PRINTF_ROUNDING_MODE=1 ./src/printf '%.0f\n' 196.5

BIN_PRINTF_ROUNDING_MODE=2 ./src/printf '%.0f\n' 196.5

Beide Befehle sollten sich in der Ausgabe unterscheiden. Die Zahlen nach IN_PRINTF_ROUNDING_MODEbedeuten:

  • 0 Rundung gegen 0
  • 1 Rundung auf die nächste Zahl (Standard)
  • 2 Rundung in Richtung positive Unendlichkeit
  • 3 Rundung gegen negative Unendlichkeit

Sie können das Ganze installieren (nicht empfohlen) oder einfach die Datei (vorheriges Umbenennen wird dringend empfohlen!) src/printfIn ein Verzeichnis in Ihrem kopieren PATHund wie oben beschrieben verwenden.

deamentiaemundi
quelle
0

Sie können den folgenden kurzen Einzeiler ausführen, wenn Sie tatsächlich für x.1 bis x.4 abrunden und für x.5 bis x.9 aufrunden möchten.

if [[ ${a#*.} -ge "5" ]]; then a=$((${a%.*}+1)); else a=${a%.*}; fi

Oder ändern Sie "5" in das, was Sie wollen, z. B. "6".

PS bezüglich des Problems mit "." und / oder "," als Dezimaltrenner verwendet, ist hier eine einfache universelle Lösung.

if [[ ${a##*[.,]} -ge "5" ]]; then a=$((${a%[.,]*}+1)); else a=${a%[.,]*}; fi

user209952
quelle