bash loop với mức tăng 0,02


11

Tôi muốn tạo một vòng lặp for trong bash với 0,02 như số gia tôi đã thử

for ((i=4.00;i<5.42;i+=0.02))
do
commands
done

nhưng nó không hoạt động.


9
Bash không làm toán học dấu phẩy động.
jordanm

1
gia tăng có thể được thực hiện bởi bc, nhưng dừng lại trên 4.52 có thể là khó khăn. sử dụng đề xuất @roaima, có var phụ trợ với bước 2 và sử dụngi=$(echo $tmp_var / 100 | bc)
Archemar


5
Bạn thường không muốn sử dụng float như một chỉ số vòng lặp . Bạn đang tích lũy lỗi trên mỗi lần lặp.
isanae

Câu trả lời:


18

Đọc bash trang người đàn ông cung cấp các thông tin sau:

for (( expr1 ; expr2 ; expr3 )) ; do list ; done

Đầu tiên, biểu thức số học expr1được đánh giá theo các quy tắc được mô tả dưới đây trong ĐÁNH GIÁ ARITHMETIC. [...]

và sau đó chúng tôi nhận được phần này

ĐÁNH GIÁ ARITHMETIC

Vỏ cho phép biểu thức số học được đánh giá, trong trường hợp nhất định (xem letdeclaređược xây dựng trong các lệnh và mở rộng số học). Đánh giá được thực hiện trong các số nguyên có chiều rộng cố định mà không cần kiểm tra tràn [...]

Vì vậy, có thể thấy rõ rằng bạn không thể sử dụng một forvòng lặp với các giá trị không nguyên.

Một giải pháp có thể chỉ đơn giản là nhân tất cả các thành phần vòng lặp của bạn với 100, cho phép điều này khi bạn sử dụng chúng sau này, như thế này:

for ((k=400;k<542;k+=2))
do
    i=$(bc <<<"scale=2; $k / 100" )    # when k=402 you get i=4.02, etc.
    ...
done

Tôi nghĩ rằng đây là giải pháp tốt nhất với k=400;k<542;k+=2nó cũng tránh được những rắc rối số học tiềm năng.
Huygens

1
Lưu ý rằng với mỗi lần lặp trong vòng lặp, bạn tạo một đường ống (để đọc đầu ra của bc), rẽ nhánh một quy trình, tạo một tệp tạm thời (cho chuỗi ở đây), thực thi bctrong đó (ngụ ý tải một thư viện thực thi và chia sẻ và khởi tạo chúng), chờ cho nó và dọn sạch. Chạy bcmột lần để làm vòng lặp sẽ hiệu quả hơn rất nhiều.
Stéphane Chazelas

@ StéphaneChazelas có, đồng ý. Nhưng nếu đây là nút cổ chai thì có lẽ chúng ta đang viết mã sai ngôn ngữ. OOI nào kém hiệu quả hơn (!)? i=$(bc <<< "scale...")hoặci=$(echo "scale..." | bc)
roaima

1
Từ thử nghiệm nhanh của tôi, phiên bản ống nhanh hơn trong zsh ( <<<đến từ đâu) bashksh. Lưu ý rằng chuyển sang shell khác bashsẽ giúp bạn tăng hiệu suất tốt hơn so với sử dụng cú pháp khác.
Stéphane Chazelas

(và hầu hết các vỏ rằng sự ủng hộ <<<(zsh, mksh, ksh93, Yash) cũng hỗ trợ arithmetics điểm nổi ( zsh, ksh93, yash)).
Stéphane Chazelas

18

Tránh các vòng trong vỏ.

Nếu bạn muốn làm số học, hãy sử dụng awkhoặc bc:

awk '
  BEGIN{
    for (i = 4.00; i < 5.42; i+ = 0.02)
      print i
  }'

Hoặc là

bc << EOF
for (i = 4.00; i < 5.42; i += 0.02)  i
EOF

Lưu ý rằng awk(trái với bc) hoạt động với doublebiểu diễn số dấu phẩy động của bộ xử lý của bạn (có thể là loại IEEE 754 ). Kết quả là, vì những số đó là xấp xỉ nhị phân của các số thập phân đó, bạn có thể có một số điều ngạc nhiên:

$ gawk 'BEGIN{for (i=0; i<=0.3; i+=0.1) print i}'
0
0.1
0.2

Nếu bạn thêm một, OFMT="%.17g"bạn có thể thấy lý do mất tích 0.3:

$ gawk 'BEGIN{OFMT="%.17g"; for (i=0; i<=0.5; i+=0.1) print i}'
0
0.10000000000000001
0.20000000000000001
0.30000000000000004
0.40000000000000002
0.5

bc không chính xác tùy ý vì vậy không có loại vấn đề này.

Lưu ý rằng theo mặc định (trừ khi bạn sửa đổi định dạng đầu ra bằng OFMThoặc sử dụng printfvới thông số định dạng rõ ràng), awksử dụng %.6gđể hiển thị số dấu phẩy động, do đó, sẽ chuyển sang 1e6 trở lên cho các số dấu phẩy động trên 1.000.000 và cắt bớt phần phân số cho số cao (100000.02 sẽ được hiển thị là 100000).

Nếu bạn thực sự cần phải sử dụng một vòng lặp vỏ, vì ví dụ bạn muốn chạy các lệnh cụ thể cho mỗi lần lặp của vòng lặp đó, hoặc sử dụng một vỏ với dấu chấm động hỗ trợ số học như zsh, yashhoặc ksh93hoặc tạo danh sách các giá trị với một lệnh như trên (hoặc seqnếu có) và lặp qua đầu ra của nó.

Giống:

unset -v IFS # configure split+glob for default word splitting
for i in $(seq 4 0.02 5.42); do
  something with "$i"
done

Hoặc là:

seq 4 0.02 5.42 | while IFS= read i; do
  something with "$i"
done

trừ khi bạn đẩy các giới hạn của số dấu phẩy động của bộ xử lý, seqxử lý các lỗi phát sinh bằng cách xấp xỉ dấu phẩy động một cách duyên dáng hơn awkphiên bản trên.

Nếu bạn không có seq(lệnh GNU), bạn có thể tạo một hàm đáng tin cậy hơn như một hàm như:

seq() { # args: first increment last
  bc << EOF
    for (i = $1; i <= $3; i += $2) i
EOF
}

Điều đó sẽ làm việc tốt hơn cho những thứ như seq 100000000001 0.000000001 100000000001.000000005. Tuy nhiên, xin lưu ý rằng việc có các số với độ chính xác cao tùy ý sẽ không giúp ích nhiều nếu chúng ta chuyển chúng cho các lệnh không hỗ trợ chúng.


Tôi đánh giá cao việc sử dụng awk! +1
Pandya

Tại sao bạn cần phải unset IFStrong ví dụ đầu tiên?
dùng1717828

@ user1717828, lý tưởng nhất là với lời mời chia tách + toàn cầu đó, chúng tôi muốn phân chia trên các ký tự dòng mới. Chúng tôi có thể làm điều đó với IFS=$'\n'nhưng điều đó không hoạt động trong tất cả các vỏ. Hoặc IFS='<a-litteral-newline-here>'nhưng điều đó không dễ đọc. Hoặc chúng ta có thể phân chia các từ thay vào đó (dấu cách, tab, dòng mới) như bạn nhận được với giá trị mặc định là $ IFS hoặc nếu bạn bỏ đặt IFS và cũng hoạt động ở đây.
Stéphane Chazelas

@ user1717828: chúng tôi không cần phải gây rối IFS, bởi vì chúng tôi biết rằng seqđầu ra của chúng không có khoảng trống mà chúng tôi cần tránh chia tách. Nó chủ yếu ở đó để đảm bảo rằng bạn nhận ra rằng ví dụ này phụ thuộc vào IFS, điều này có thể quan trọng đối với một lệnh tạo danh sách khác.
Peter Cordes

1
@PeterCordes, nó ở đó vì vậy chúng tôi không cần đưa ra bất kỳ giả định nào về những gì IFS đã được đặt trước.
Stéphane Chazelas


0

Như những người khác đã đề xuất, bạn có thể sử dụng bc:

i="4.00"

while [[ "$(bc <<< "$i < 5.42")" == "1" ]]; do
    # do something with i
    i="$(bc <<< "$i + 0.02")"
done
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.