петля Баш с шагом 0,02


11

Я хочу сделать цикл для Bash с 0,02 в качестве приращений я пытался

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

но это не сработало.


9
Bash не занимается математикой с плавающей запятой.
Иордания

1
увеличение может быть сделано bc, но остановка на 4.52 может быть хитрой. используйте предложение @roaima, используйте вспомогательную переменную с шагом 2 и используйтеi=$(echo $tmp_var / 100 | bc)
Archemar


5
Обычно вы не хотите использовать float в качестве индекса цикла . Вы накапливаете ошибку на каждой итерации.
Исана

Ответы:


18

Чтение справочной bash страницы дает следующую информацию:

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

Во-первых, арифметическое выражение expr1оценивается в соответствии с правилами, описанными ниже в разделе АРИФМЕТИЧЕСКАЯ ОЦЕНКА. [...]

и тогда мы получим этот раздел

АРИФМЕТИЧЕСКАЯ ОЦЕНКА

Оболочка позволяет арифметические выражения для оценки, при определенных обстоятельствах (см letи declareвстроенных команды и Арифметическое расширение). Оценка выполняется в целых числах фиксированной ширины без проверки на переполнение [...]

Таким образом, ясно видно, что вы не можете использовать forцикл с нецелыми значениями.

Одним из решений может быть простое умножение всех ваших компонентов цикла на 100, что позволяет использовать их там, где вы позже будете их использовать, например:

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

Я думаю, что это лучшее решение, k=400;k<542;k+=2поскольку оно позволяет избежать потенциальных арифметических проблем с плавающей запятой.
Гюйгенс

1
Обратите внимание, что для каждой итерации в цикле вы создаете канал (для чтения выходных данных bc), разветвляете процесс, создаете временный файл (для строки here), выполняете bcв нем (что подразумевает загрузку исполняемых и разделяемых библиотек и инициализирую их), дождемся его и приведем в порядок. Запускать bcодин раз, чтобы сделать цикл, будет намного эффективнее.
Стефан Шазелас

@ StéphaneChazelas да, согласился. Но если это узкое место, то, вероятно, мы все равно пишем код на неправильном языке. OOI, который менее неэффективен (!)? i=$(bc <<< "scale...")илиi=$(echo "scale..." | bc)
Ройма

1
Из моего быстрого теста, версия канала быстрее в zsh (откуда <<<взялась), bashи ksh. Обратите внимание, что переключение на другую оболочку, чем bashв любом случае, даст вам большее повышение производительности, чем использование другого синтаксиса.
Стефан Шазелас

(и большинство из оболочек , что поддержка <<<(ЗШ, МКШ, ksh93, йаш) также поддерживает арифметику с плавающей запятой ( zsh, ksh93, yash)).
Стефан Шазелас

18

Избегайте петель в ракушках.

Если вы хотите сделать арифметику, используйте awkили bc:

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

Или

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

Обратите внимание, что awk(вопреки bc) работает с вашими процессорами представления doubleчисел с плавающей запятой (вероятно, типа IEEE 754 ). В результате, поскольку эти числа являются двоичными приближениями этих десятичных чисел, у вас могут быть некоторые сюрпризы:

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

Если вы добавите, OFMT="%.17g"вы увидите причину пропажи 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 имеет произвольную точность, поэтому не имеет такой проблемы.

Обратите внимание, что по умолчанию (если вы не изменяете выходной формат с помощью OFMTили не используете printfявные спецификации формата), awkиспользуется %.6gдля отображения чисел с плавающей запятой, поэтому переключился бы на 1e6 и выше для чисел с плавающей запятой свыше 1 000 000 и урезал дробную часть для больших чисел (100000.02 будет отображаться как 100000).

Если вам действительно нужно использовать цикл оболочки, так как , например , вы хотите запустить определенные команды для каждой итерации этого цикла, либо использовать оболочку с плавающей запятой арифметической поддержки , как zsh, yashили ksh93или создать список значений с помощью одной команды , как указаны выше (или, seqесли доступно) и переберите его вывод.

Подобно:

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

Или:

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

если вы не увеличите пределы чисел с плавающей запятой вашего процессора, то seqошибки, возникшие из-за аппроксимации с плавающей запятой, будут обрабатываться более изящно, чем awkвышеприведенная версия.

Если у вас нет seq(команда GNU), вы можете сделать более надежную функцию как:

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

Это будет работать лучше для таких вещей, как seq 100000000001 0.000000001 100000000001.000000005. Однако обратите внимание, что наличие чисел с произвольно высокой точностью мало поможет, если мы собираемся передать их командам, которые их не поддерживают.


Я ценю использование awk! +1
Pandya

Зачем вам нужно unset IFSв первом примере?
user1717828

@ user1717828, в идеале, с этим вызовом split + glob мы хотим разделить символы новой строки. Мы можем сделать это с, IFS=$'\n'но это работает не во всех оболочках. Или, IFS='<a-litteral-newline-here>'но это не очень разборчиво. Или мы можем вместо этого разделить слова (пробел, табуляция, новая строка), как вы получаете со значением по умолчанию $ IFS, или если вы сбросили IFS и также работаете здесь.
Стефан Шазелас

@ user1717828: нам не нужно связываться IFS, потому что мы знаем, что в seqвыводе нет пробелов, которые мы должны избежать, чтобы не разбиваться. В основном это делается для того, чтобы убедиться, что вы понимаете, от IFSчего зависит этот пример , что может иметь значение для другой команды генерации списка.
Питер Кордес

1
@PeterCordes, он там, поэтому нам не нужно делать никаких предположений о том, что IFS был установлен заранее.
Стефан Шазелас


0

Как предлагали другие, вы можете использовать bc:

i="4.00"

while [[ "$(bc <<< "$i < 5.42")" == "1" ]]; do
    # do something with i
    i="$(bc <<< "$i + 0.02")"
done
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.