Seltsames Float-Rundungsverhalten mit printf

Seltsames Float-Rundungsverhalten mit printf

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

Bei der praktischen Verwendung führte jedoch 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 die Rundung 196.5zu wird 196.

Ich weiß, dass es sich hier um einen subtilen Gleitkommafehler handeln kann (aber es handelt sich nicht um eine sehr große Zahl, oder?). Kann also jemand Licht in die Sache bringen?

Eine Problemumgehung hierfür wäre ebenfalls sehr willkommen (da ich gerade versuche, dies umzusetzen).

Antwort1

Es ist wie erwartet, es wird auf den Betrag aufgerundet oder die sogenannte Banker-Rundung angewendet.

AVerwandte Site-Antworterkläre es.

Das Problem, das mit dieser Regel gelöst werden soll, ist, dass (für Zahlen mit einer Dezimalstelle)

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

Das sind 4 nach unten und 4 nach oben.
Um die Rundung im Gleichgewicht zu halten, müssen wir die x,5 aufrunden.

  • hocheinmal undrunterder nächste.

Dies geschieht durch die 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 Bankiersregel.
  2. Runden Sie in Richtung +unendlich. Aufrunden (für positive Zahlen)
  3. Runden Sie in Richtung -unendlich. Abrunden Sie (für positive Zahlen)
  4. Runden Sie auf Null. Entfernen Sie die Dezimalstellen (positiv oder negativ).

Hoch

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

Runter

Wenn Sie „abrunden (in 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.

Um die Dezimalstellen (alles nach dem Punkt) zu entfernen.
Wir könnten auch direkt die Shell verwenden (funktioniert auf 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

Antwort2

Das ist kein Fehler, sondern Absicht.
Es wird eine Art Rundung auf die nächste Zahl durchgeführt (mehr dazu später).
Mit „exakt“ .5können wir in beide Richtungen runden. In der Schule wurde Ihnen wahrscheinlich gesagt, dass Sie aufrunden sollen, aber warum? Weil Sie dann keine weiteren Ziffern mehr untersuchen müssen, z. B. 3,51 auf 4 aufrunden; 3,5 könnte in beide Richtungen gehen, aber wenn wir nur die erste Ziffer betrachten und 0,5 aufrunden, dann machen wir es immer richtig.

Wenn wir uns jedoch die zweistelligen Dezimalzahlen ansehen: 0,00 0,01, 0,02, 0,03 … 0,98, 0,99, sehen wir, dass es 100 Werte gibt, 1 ist eine Ganzzahl, 49 müssen aufgerundet werden, 49 müssen abgerundet werden, 1 ( 0,50 ) könnte in beide Richtungen 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, die aufgerundet werden. Dadurch wird unser Durchschnitt etwas höher als erwartet. Ein Versuch, dies zu beheben, ist: .5 wird in Richtung der geraden Zahl gerundet. Die Hälfte der Zeit wird aufgerundet, die andere Hälfte abgerundet.

Dadurch ändert sich die Tendenz von aufwärts nach hin zu gleichmäßig. In den meisten Fällen ist das besser.

Antwort3

Das vorübergehende Ändern der Rundungsmodi ist nicht ungewöhnlich und es ist möglich, bin/printfwenn auch nichtan sichSie müssen die Quellen ändern.

Sie benötigen die Quellen der Coreutils. Ich habe die aktuellste verfügbare Version verwendet, diehttp://ftp.gnu.org/gnu/coreutils/coreutils-8.24.tar.xz.

Entpacken Sie in ein Verzeichnis Ihrer Wahl mit

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 ersetzen Sie die gesamte mainFunktion durch die folgende Funktion, einschließlich der beiden Präprozessoranweisungen zum Einbinden der Headerdateien math.hund fenv.h. Die Hauptfunktion steht am Ende und beginnt bei int main...und endet ganz 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 dies ./configurewie folgt aus

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

Es fügt das Suffix -ownan jedes Unterprogramm (es gibt viele davon) an, falls Sie sie alle installieren möchten und sich nicht sicher sind, ob sie zum Rest des Systems passen. Die Coreutils sind nicht benanntKernUtilities ohne Grund!

Aber das Wichtigste ist das LIBS=-lmvor der Zeile. Wir brauchen die mathematische Bibliothek und dieser Befehl weist uns an, ./configuresie zur Liste der benötigten Bibliotheken hinzuzufügen.

Führen Sie make aus

make

Wenn Sie ein Multicore-/Multiprozessorsystem haben, versuchen Sie

make -j4

wobei die Zahl (hier „4“) die Anzahl der Kerne darstellen sollte, die Sie für diesen Job freihalten möchten.

Wenn alles gut gegangen ist, haben Sie das neue printfint src/printf. Probieren Sie es aus:

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

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

Die Ausgabe beider Befehle sollte unterschiedlich sein. Die Zahlen dahinter IN_PRINTF_ROUNDING_MODEbedeuten:

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

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

Antwort4

Sie können den folgenden kurzen Einzeiler verwenden, wenn Sie tatsächlich von x,1 auf x,4 abrunden und von x,5 auf x,9 aufrunden möchten.

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

Oder ändern Sie „5“ in etwas anderes als „6“.

PS: Zum Problem mit „.“ und/oder „“, die als Dezimaltrennzeichen verwendet werden, gibt es hier eine einfache, universelle Lösung.

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

verwandte Informationen