Hành vi làm tròn nổi lạ với printf


7

Tôi đã đọc một số câu trả lời trên trang web này và thấy printflàm tròn mong muốn.

Tuy nhiên, khi tôi sử dụng nó trong thực tế, một lỗi tinh vi đã dẫn tôi đến hành vi sau:

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

Lưu ý rằng làm tròn 196.5trở thành 196.

Tôi biết đây có thể là một số lỗi dấu phẩy động tinh tế (nhưng đây không phải là một con số rất lớn, phải không?), Vì vậy ai đó có thể đưa ra ánh sáng về điều này không?

Một cách giải quyết cho việc này cũng được hoan nghênh rất nhiều (vì tôi đang cố gắng để điều này hoạt động ngay bây giờ).


Bạn thấy một lỗi ở đâu? 196 là một làm tròn hoàn toàn hợp lý từ 196,5 đến 0 vị trí sau dấu thập phân và ditto cho 198 cho 197,5. Bạn có tự hỏi tại sao đôi khi nó tròn lên và đôi khi xuống? Điều đó là bình thường vì đây là làm tròn, không cắt ngắn.
Gilles 'SO- ngừng trở nên xấu xa'


@Gilles Vâng, ý tôi là có một lỗi trong kịch bản của riêng tôi liên quan đến số học dấu phẩy động bởi vì nó không hoạt động như những gì kịch bản thực sự yêu cầu, và tôi biết tất nhiên đây không phải là lỗi trong IEEE 754 ...
Hai Zhang

Câu trả lời:


15

Đúng như dự đoán, đó là "làm tròn đến chẵn" hoặc "Làm tròn ngân hàng".

Một câu trả lời trang web liên quan giải thích nó.

Vấn đề mà quy tắc đó đang cố gắng giải quyết là (đối với các số có một số thập phân),

  • x.1 lên đến x.4 được làm tròn xuống.
  • x.6 lên đến x.9 được làm tròn lên.

Đó là 4 xuống và 4 lên.
Để giữ cho số tròn cân bằng, chúng ta cần làm tròn x.5

  • lên một lần và xuống lần sau

Điều đó được thực hiện theo quy tắc: «Làm tròn đến 'số chẵn' gần nhất» ».

Trong mã:

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


Tùy chọn:

Tổng cộng, có bốn cách có thể để làm tròn một số:

  1. Quy tắc đã được giải thích của Ngân hàng.
  2. Vòng về phía + vô hạn. Làm tròn số (đối với số dương)
  3. Vòng về phía vô tận. Làm tròn xuống (đối với số dương)
  4. Vòng về phía không. Loại bỏ các số thập phân (dương hoặc âm).

Lên

Nếu bạn cần "làm tròn (hướng tới +infinite)", thì bạn có thể sử dụng awk:

value=195.5

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

Xuống

Nếu bạn cần "làm tròn xuống (Hướng tới -infinite)", thì bạn có thể sử dụng:

value=195.5

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

Cắt số thập phân.

Để loại bỏ số thập phân (bất cứ điều gì sau dấu chấm).
Chúng tôi cũng có thể trực tiếp sử dụng shell (hoạt động trên hầu hết các shell - là POSIX):

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

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


4

Nó không phải là một lỗi, nó là cố ý.
Nó đang thực hiện một loại vòng để gần nhất (nhiều hơn về sau).
Với chính xác .5chúng ta có thể làm tròn một trong hai cách. Ở trường bạn có lẽ bảo phải làm tròn, nhưng tại sao? Bởi vì sau đó bạn không phải kiểm tra thêm bất kỳ chữ số nào, ví dụ 3,51 làm tròn đến 4; 3.5 có thể đi theo cách của ether, nhưng nếu chúng ta chỉ nhìn vào chữ số đầu tiên và làm tròn 0,5 thì chúng ta luôn hiểu đúng.

Tuy nhiên, nếu nhìn vào tập hợp các số thập phân gồm 2 chữ số: 0,00 0,01, 0,02, 0,03 0,98, 0,99, chúng ta sẽ thấy có 100 giá trị, 1 là số nguyên, 49 phải được làm tròn lên, 49 phải được làm tròn xuống , 1 (0,50) có thể đi theo cách ether. Nếu chúng ta luôn làm tròn, thì chúng ta nhận được số trung bình là 0,01 quá lớn.

Nếu chúng tôi mở rộng phạm vi lên 0 → 9,99, chúng tôi có thêm 9 giá trị làm tròn lên. Do đó làm cho trung bình của chúng tôi lớn hơn một chút so với dự kiến. Vì vậy, một nỗ lực để khắc phục điều này là: 0,5 vòng về phía chẵn. Một nửa thời gian nó làm tròn lên, một nửa thời gian nó làm tròn xuống.

Điều này thay đổi sự thiên vị từ hướng lên, sang hướng chẵn. Trong hầu hết các trường hợp điều này là tốt hơn.


1

Tạm thời thay đổi phương thức làm tròn mà không phải là bất thường và có thể với bin/printfmặc dù không phải cho mỗi gia nhập bạn cần phải thay đổi nguồn.

Bạn cần các nguồn của coreutils, tôi đã sử dụng phiên bản mới nhất hiện nay là http://ftp.gnu.org/gnu/coreutils/coreutils-8.24.tar.xz .

Giải nén vào một thư mục bạn chọn với

tar xJfv coreutils-8.24.tar.xz

Thay đổi vào thư mục nguồn

cd coreutils-8.24

Tải tệp src/printf.cvào trình soạn thảo bạn chọn và trao đổi toàn bộ mainchức năng với chức năng sau bao gồm cả hai chỉ thị tiền xử lý để bao gồm các tệp tiêu đề math.hfenv.h. Hàm chính nằm ở cuối và bắt đầu tại int main...và kết thúc ở cuối tệp với dấu ngoặc đóng}

#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;
}

Chạy ./configurenhư sau

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

Nó đặt hậu tố -ownở mọi chương trình con (có rất nhiều) chỉ trong trường hợp bạn muốn cài đặt tất cả và không chắc chắn liệu chúng có phù hợp với phần còn lại của hệ thống không. Coreutils không được đặt tên là dụng cụ cốt lõi mà không có lý do!

Nhưng quan trọng nhất là LIBS=-lmở phía trước của dòng. Chúng ta cần thư viện toán học và lệnh này yêu ./configurecầu thêm nó vào danh sách các thư viện cần thiết.

Chạy đi

make

Nếu bạn có một hệ thống đa lõi / đa bộ xử lý hãy thử

make -j4

trong đó số (ở đây là "4") sẽ đại diện cho số lõi bạn sẵn sàng dành cho công việc đó.

Nếu mọi việc suôn sẻ, bạn có printfint mới src/printf. Dùng thử:

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

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

Cả hai lệnh sẽ khác nhau ở đầu ra. Các số sau IN_PRINTF_ROUNDING_MODEtrung bình:

  • 0 Làm tròn về 0
  • 1 Làm tròn về số gần nhất (mặc định)
  • 2 Làm tròn về phía vô cực tích cực
  • 3 Làm tròn về phía vô cực âm

Bạn có thể cài đặt toàn bộ (không được khuyến nghị) hoặc chỉ sao chép tệp (đổi tên trước đó rất được khuyến khích!) src/printfVào một thư mục trong của bạn PATHvà sử dụng như mô tả ở trên.


0

Bạn có thể thực hiện một đoạn ngắn sau đây nếu điều bạn thực sự muốn làm tròn xuống cho x.1 đến x.4 và làm tròn cho x.5 đến x.9.

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

Hoặc thay đổi "5" thành bất cứ điều gì bạn muốn, ví dụ "6".

PS liên quan đến vấn đề với "." và / hoặc "," đang được sử dụng làm (các) dấu thập phân, đây là một giải pháp phổ quát dễ dàng.

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

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.