Bash if-Bedingungen von Variablen innerhalb von Intervallen

Bash if-Bedingungen von Variablen innerhalb von Intervallen

Ich habe einige Daten, die über Zeitintervalle verteilt sind, und ich möchte einige dieser Daten innerhalb der Zeitintervalle erfassen. Beispielsweise habe ich Daten zu Zeitpunkten zwischen 1..9, 11..19 usw. und ich möchte Daten zwischen 1-2, dann zwischen 11-12 usw. erfassen.

Dies wird Teil eines komplexeren bashSkripts sein, in das ich diese Bedingung mit einem Zyklus einbinden möchte if, um die Zeiten zu isolieren, in denen ich die Daten erfassen kann.

Ich dachte an so etwas wie:

if (( $t_initial & $t_final )) in (begin1, fin1) or in (begin2, fin2) ...

wobei t_initialund t_finalseparat vom Skript selbst berechnet werden.

Ich kann diese Bedingung nicht in bashSyntax schreiben. Ich habe einige andere Lösungen gefunden, aber sie scheinen extrem lang und unpraktisch zu sein, deshalb bin ich hier, um nach einfacheren und lesbareren Lösungen zu fragen.


Es ist wichtig, dass der Code mit Floats richtig funktioniert. Ich versuche, die OP-Lösung dafür zu reparieren, finde aber immer noch keinen Weg.

Antwort1

Ich bin nicht sicher, ob Ihnen diese Lösung gefällt. Sie hat zwei Funktionen:

  • Es sind keine externen Programme erforderlich
  • Es verwendet eine Funktion, so dass zumindest die Komplexität der Vergleiche verborgen bleibt

Das ist es:

#!/bin/bash

# The comparing function
function compareInterval {
 t1=$1
 t2=$2

 shift 2

 while (( "$2" )); do
   if ((  $t1 >= $1  &&  $t2 <= $2 )); then
     # got match
     return 0
   fi
   shift 2
 done

 return 1
}

# sample values
t_initial=2
t_final=4

# Invocation. Compares against 1-3, 3-5, 2-5
if compareInterval  $t_initial $t_final  1 3  3 5  2 5; then
 echo Got match
fi

Antwort2

bashunterstützt weder einen Bereichsvergleich noch Gleitkommazahlen von Haus aus, daher müssen wir einiges davon selbst erledigen. Ich werde auch eine Funktion definieren und sie bcfür Gleitkommaberechnungen verwenden. Hier ist das Endergebnis und die Testsuite:

# Call as `compareRanges start end b1 f1 b2 f2 b3 f3...`
compareRanges() {
    local t_initial=$1
    local t_final=$2
    shift 2
    while [ ${#@} -gt 1 ]
    do
        local in_range=$(bc <<<"$t_initial >= $1 && $t_final <= $2")
        if [ $in_range = 1 ]
        then
            # Debugging output to stderr - can be removed:
            echo "[$t_initial,$t_final] is within [$1,$2]" >&2
            return 0
        fi
        shift 2
    done
    # Debugging output to stderr - can be removed:
    echo "[$t_initial,$t_final] is not within any ranges." >&2
    return 1
}
# Basic integers from the example
compareRanges 1 3 2 4 && echo BAD || echo OK
compareRanges 1 3 1 3 && echo OK || echo BAD
compareRanges 1 3 0 4 && echo OK || echo BAD
# Fractional numbers
compareRanges 1.5 2.5 1.1 2.2 && echo BAD || echo OK
compareRanges 1.5 2.5 0.3 3.1 && echo OK || echo BAD
# Multiple ranges
compareRanges 5 7 1 4 2 6 3 9 && echo OK || echo BAD
compareRanges 5 7 1 2 3 4 5 6 7 8 && echo BAD || echo OK

Die compareRangesFunktion nimmt mindestens zwei Argumente an. Das erste ist Ihr t_initial, und das zweite ist Ihr t_final. Danach kann sie beliebig viele andere Argumente paarweise annehmen, und zwar in der Reihenfolge Ihre begin1, fin1, begin2, fin2, .

Der erste Testfall vergleicht die Bereiche in den Kommentaren zur Frage: 1-3 und 2-4.

compareRanges 1 3 2 4 && echo BAD || echo OK

So 1ist t_initial, 3ist t_final, 2ist begin1und 4ist fin1.

Wenn Sie mehrere Bereiche verwenden möchten, listen Sie diese anschließend alle paarweise auf:

compareRanges 5 7 1 4 2 6 3 9 && echo OK || echo BAD

Hier testen wir gegen 1-4, 2-6 und 3-9. In der whileSchleife betrachten wir jedes Paar der Reihe nach und vergleichen es mit t_initialund t_final.

Da bashkeine Bruchzahlen unterstützt werden, verwenden wirbc, ein Rechner mit beliebiger Genauigkeit. Seine Eingabe wird durch den <<<"$t_initial >= $1" ...Teil bereitgestellt: der den String in die Standardeingabe einspeist. $1ist der Anfang des Bereichs, den wir uns gerade in dieser Iteration der Schleife ansehen, und $2ist das Ende; wir vergleichen sowohl die Unter- als auch die Obergrenze gleichzeitig mit &&. bcgibt aus, 1wenn die Vergleiche wahr sind, und 0wenn einer falsch ist. Wir speichern das Ergebnis in in_range, und die Funktion ist erfolgreich ( return 0), wenn beide unserer Tests wahr waren.

Bruchzahlen können einfach in ihrer normalen Dezimalform angegeben werden:

compareRanges 1.5 2.5 0.3 3.1 && echo OK || echo BAD

bcverarbeitet Zahlen mit beliebig vielen Nachkommastellen und in der von Ihnen benötigten Größenordnung.

Wenn am Ende keines der Grenzpaare übereinstimmt, schlagen wir fehl ( return 1). Sie können die Funktion wie folgt verwenden:

if compareRanges $t_initial $t_final 2 4 11 19
then
    ...
fi

Die Testsuite sollte beim Ausführen ausschließlich „OK“ ausgeben.


Alternativ können auch andere Schalen (wiezsh)Tununterstützt gebrochene Variablenwerte. Wenn Sie Ihr Skript in einem dieser Formate ausführen könnten, könnten Sie die Verwendung von vermeiden bc, obwohl der Vergleich in einer Funktion immer noch besser ist. Zumindest in zshdiesem Fall handelt es sich um Gleitkommazahlen, sodass sie nicht unbedingt genau sind; bcwird immer korrekt sein.

Antwort3

Hier sind ein paar schamlose Nachahmungen von LatinSuDs Antwort, die mit Gleitkommazahlen umgehen. Sie werden bemerken, dass seine Antwort damit prahlt: „Es sind keine externen Programme erforderlich.“ Diese hier verwendet das Taschenrechnerprogramm, bcwie von ihm vorgeschlagen:

#!/bin/bash

# The comparing function
function compareInterval {
 t1=$1
 t2=$2

 shift 2

 while (( "$2" ))
 do
   # if ((  $t1 >= $1  &&  $t2 <= $2 ))
   bc_result=$(echo "print $t1 >= $1  &&  $t2 <= $2" | bc)
   if [  "$bc_result" = 1 ]
   then
     # got match
     return 0
   fi
   shift 2
 done

 return 1
}

# sample values
t_initial=2.3
t_final=4.2

# Invocation. Compares against 1-3, 3-5, 2-5
if compareInterval  $t_initial $t_final  1 3  3 5  2 5
then
 echo Got match
fi

Dadurch wird der Test einfach übernommen if (( $t1 >= $1 && $t2 <= $2 ))und an gesendet bc. Anschließend wird die Ausgabe von erfasst bc.


Ein anderer Ansatz besteht darin, die Zahlen durch Multiplikation mit einer Zehnerpotenz auf Ganzzahlen zu normalisieren. Dies erfordert eine maximale Anzahl von Dezimalstellen. Wenn beispielsweise kein Datenpunkt mehr als drei Ziffern rechts vom Dezimalpunkt hat, können wir alles mit 1000 multiplizieren.

#!/bin/bash

# Normalize function: it multiplies a floating point number by 1000
# without using floating point arithmetic.
normalize()
{
  case "$1" in
    *.*)
        result=$(echo "$1"000 | sed 's/\(.*\)\.\(...\).*/\1\2/')
        ;;
    *)
        result="$1"000
  esac
  echo "$result"
}

# The comparing function
function compareInterval {
 t1=$(normalize $1)
 t2=$(normalize $2)

 shift 2

 while (( "$2" ))
 do
   a1=$(normalize $1)
   a2=$(normalize $2)
   if ((  $t1 >= $a1  &&  $t2 <= $a2 ))
   then
     # got match
     return 0
   fi
   shift 2
 done

 return 1
}

# sample values
t_initial=2.3
t_final=4.2

# Invocation. Compares against 1-3, 3-5, 2-5
if compareInterval  $t_initial $t_final  1 3  3 5  2 5
then
 echo Got match
fi

Wenn der Parameter der normalizeFunktion eine einfache Ganzzahl ist (also eine Zahl ohne Dezimalpunkt, z. B. 17), können wir einfach mit 1000 multiplizieren, indem wir anhängen 000, also 1717000. Wenn der Parameter normalizeeine Gleitkommazahl ist (also einen Dezimalpunkt enthält, z. B. 42.5), hängen wir trotzdem an 000und verwenden dann sed, um den Dezimalpunkt und alles nach der dritten Ziffer zu entfernen. Der sedBefehl s/\(.*\)\.\(...\).*/\1\2/nimmt einen String wieabcdef.Abonnieren und RetourenAbonnieren, also 42.542.500042500(dh 42,5 × 1000).

Es ist möglicherweise möglich, diese Zeichenfolgenmanipulation vollständig in durchzuführen bash, ohne zu verwenden sed.

Antwort4

Die Sache mit den „11..19“, nach der Sie fragen, wird als Klammererweiterung bezeichnet.

Sie können entweder verwenden eval {$t_initial..$t_final},

...oder

if `seq $t_initial..$t_final`==$somevalue

verwandte Informationen