bash loop dengan kenaikan 0,02

11

Saya ingin membuat for for loop di bash dengan 0,02 sebagai peningkatan yang saya coba

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

tapi itu tidak berhasil.

Mehrshad
sumber
9
Bash tidak melakukan matematika titik mengambang.
jordanm
1
kenaikan bisa dibuat oleh bc, tetapi berhenti pada 4,52 mungkin sulit. gunakan saran @roaima, dapatkan var bantu dengan langkah 2, dan gunakani=$(echo $tmp_var / 100 | bc)
Archemar
5
Anda biasanya tidak ingin menggunakan pelampung sebagai indeks lingkaran . Anda mengumpulkan kesalahan pada setiap iterasi.
isanae

Jawaban:

18

Membaca bash halaman manual memberikan informasi berikut:

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

Pertama, ekspresi aritmatika expr1dievaluasi sesuai dengan aturan yang dijelaskan di bawah ini di bawah EVALUASI ARITHMETIC. [...]

dan kemudian kita mendapatkan bagian ini

EVALUASI ARITHMETIK

Shell memungkinkan ekspresi aritmatika dievaluasi, dalam keadaan tertentu (lihat perintah letdan declarebuiltin dan Ekspansi Aritmatika). Evaluasi dilakukan dalam bilangan bulat tetap-lebar tanpa pemeriksaan limpahan [...]

Jadi dapat terlihat jelas bahwa Anda tidak dapat menggunakan forloop dengan nilai-nilai non-integer.

Satu solusi mungkin hanya dengan melipatgandakan semua komponen loop Anda dengan 100, memungkinkan untuk ini di mana Anda nanti menggunakannya, seperti ini:

for ((k=400;k<542;k+=2))
do
    i=$(bc <<<"scale=2; $k / 100" )    # when k=402 you get i=4.02, etc.
    ...
done
roaima
sumber
Saya pikir ini adalah solusi terbaik dengan k=400;k<542;k+=2itu juga menghindari masalah aritmatika floating point.
Huygens
1
Perhatikan bahwa untuk setiap iterasi dalam loop, Anda membuat pipa (untuk membaca output bc), melakukan proses, membuat file sementara (untuk string-sini), jalankan bcdi dalamnya (yang menyiratkan memuat perpustakaan yang dapat dieksekusi dan dibagikan, serta menginisialisasi mereka), tunggu dan bersih-bersih. Menjalankan bcsekali untuk melakukan loop akan jauh lebih efisien.
Stéphane Chazelas
@ StéphaneChazelas ya, setuju. Tetapi jika ini adalah bottleneck maka kita mungkin menulis kode dalam bahasa yang salah pula. OOI yang kurang efisien (!)? i=$(bc <<< "scale...")ataui=$(echo "scale..." | bc)
roaima
1
Dari tes cepat saya, versi pipa lebih cepat di zsh (dari mana <<<asalnya), bashdan ksh. Perhatikan bahwa beralih ke shell lain selain bashakan memberi Anda peningkatan kinerja yang lebih baik daripada menggunakan sintaks lainnya.
Stéphane Chazelas
(dan sebagian besar kerang yang support <<<(zsh, mksh, ksh93, yash) juga mendukung floating point aritmatika ( zsh, ksh93, yash)).
Stéphane Chazelas
18

Hindari loop dalam cangkang.

Jika Anda ingin melakukan aritmatika, gunakan awkatau bc:

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

Atau

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

Perhatikan bahwa awk(bertentangan dengan bc) bekerja dengan doublerepresentasi nomor floating point prosesor Anda (kemungkinan tipe IEEE 754 ). Akibatnya, karena angka-angka itu adalah perkiraan biner dari angka desimal itu, Anda mungkin memiliki beberapa kejutan:

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

Jika Anda menambahkan, OFMT="%.17g"Anda dapat melihat alasan hilangnya 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 tidak presisi sewenang-wenang sehingga tidak memiliki masalah seperti ini.

Perhatikan bahwa secara default (kecuali jika Anda memodifikasi format output dengan OFMTatau menggunakan printfdengan spesifikasi format eksplisit), awkdigunakan %.6guntuk menampilkan angka floating point, jadi akan beralih ke 1e6 dan di atasnya untuk angka floating point di atas 1.000.000 dan memotong bagian fraksional untuk angka tinggi (100000.02 akan ditampilkan sebagai 100000).

Jika Anda benar-benar perlu menggunakan shell loop, karena misalnya Anda ingin menjalankan perintah khusus untuk setiap iterasi dari loop itu, baik menggunakan shell dengan dukungan aritmatika floating point seperti zsh, yashatau ksh93atau menghasilkan daftar nilai dengan satu perintah seperti di atas (atau seqjika tersedia) dan lewati outputnya.

Suka:

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

Atau:

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

kecuali Anda menekan batas angka floating point prosesor Anda, seqmenangani kesalahan yang ditimbulkan oleh pendekatan floating point lebih anggun daripada awkversi di atas.

Jika Anda tidak memiliki seq(perintah GNU), Anda dapat membuat yang lebih dapat diandalkan sebagai fungsi seperti:

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

Itu akan bekerja lebih baik untuk hal-hal seperti seq 100000000001 0.000000001 100000000001.000000005. Namun perlu dicatat bahwa memiliki angka dengan presisi tinggi yang sewenang-wenang tidak akan banyak membantu jika kita akan meneruskannya ke perintah yang tidak mendukungnya.

Stéphane Chazelas
sumber
Saya menghargai penggunaan awk! +1
Pandya
Mengapa Anda perlu unset IFSpada contoh pertama?
user1717828
@ user1717828, idealnya, dengan seruan split + glob tersebut, kami ingin memisahkan karakter baris baru. Kita bisa melakukannya dengan IFS=$'\n'tetapi itu tidak berhasil di semua shell. Atau IFS='<a-litteral-newline-here>'tapi itu tidak terlalu terbaca. Atau kita dapat membagi kata-kata sebagai gantinya (spasi, tab, baris baru) seperti yang Anda dapatkan dengan nilai default $ IFS atau jika Anda menghapus IFS dan juga berfungsi di sini.
Stéphane Chazelas
@ user1717828: kita tidak perlu dipusingkan IFS, karena kita tahu bahwa seqoutput tidak memiliki ruang yang harus kita hindari pemisahan. Sebagian besar ada di sana untuk memastikan Anda menyadari bahwa contoh ini tergantung IFS, yang mungkin penting untuk perintah pembuatan daftar yang berbeda.
Peter Cordes
1
@PeterCordes, itu ada di sana sehingga kita tidak perlu membuat asumsi tentang apa IFS diatur sebelumnya.
Stéphane Chazelas
2

Gunakan "seq" - mencetak urutan angka

seq INCREMENT PERTAMA TERAKHIR

for i in $(seq 4.00 0.02 5.42)
do 
  echo $i 
done
borzol
sumber
Jawaban itu sudah diberikan .
Stéphane Chazelas
0

Seperti yang disarankan orang lain, Anda dapat menggunakan bc:

i="4.00"

while [[ "$(bc <<< "$i < 5.42")" == "1" ]]; do
    # do something with i
    i="$(bc <<< "$i + 0.02")"
done
Andy Dalton
sumber