Error en la función de shell para contar números pares


12

Para una tarea, tengo que escribir una función que imprima el número de números pares cuando se proporciona una secuencia de números.

Usé el código que usé para una tarea anterior (para imprimir 1cuando un número era par y 0cuando el número era impar)

Mi problema ahora es que mi función sigue imprimiendo 0. ¿Qué estoy haciendo mal?

Aquí está mi guión:

#!/usr/bin/bash
# File: nevens.sh

# Write a function called nevens which prints the number of even numbers when provided with a sequence of numbers.
# Check: input nevens 42 6 7 9 33 = output 2

function nevens {

        local sum=0

        for element in $@
        do
                let evencheck=$(( $# % 2 ))
                if [[ $evencheck -eq 0 ]]
                then
                        let sum=$sum+1
                fi
        done

        echo $sum
}

2
escriba en su shebang '#! / usr / bin / bash -x' y verá lo que sucede exactamente.
Ziazis

+1 por decirnos que es tarea, y por haber trabajado lo suficientemente duro como para merecer ayuda.
Joe

Respuestas:


20

Simplemente olvidó reemplazar $#con ( $) elementen el forbucle:

function nevens {
  local sum=0
  for element in $@; do
    let evencheck=$(( element % 2 ))
    if [[ $evencheck -eq 0 ]]; then
      let sum=sum+1
    fi
  done
  echo $sum
}

Ahora para probar la función:

$ nevens 42 6 7 9 33
2
$ nevens 42 6 7 9 33 22
3
$ nevens {1..10..2} # 1 to 10 step 2 → odd numbers only
0
$ nevens {2..10..2} # 2 to 10 step 2 → five even numbers
5

17

@dessert ha encontrado el problema central, daré una revisión de código:

  1. El shebang: No hay /usr/bin/bashen Ubuntu. Es /bin/bash.
  2. Es bueno que hayas declarado sum localy evitado contaminar el espacio de nombres variable fuera de la función. Además, puede declararlo como una variable entera utilizando la -iopción:

    local -i sum=0
  3. ¡Siempre cita tus variables (y parámetros)! No es necesario en este script, pero es un muy buen hábito para entrar:

    for element in "$@"
    do

    Dicho esto, puedes omitir el in "$@"aquí:

    for element
    do

    Cuando in <something>no se proporciona, el forbucle recorre implícitamente los argumentos. Esto puede evitar errores como olvidar las comillas.

  4. No hay necesidad de calcular y luego verificar el resultado. Puedes hacer el cálculo directamente en if:

    if (( (element % 2) == 0 ))
    then
        ((sum = sum + 1))
    fi

    (( ... ))Es el contexto aritmético . Es más útil que [[ ... ]]para realizar comprobaciones aritméticas, y además puede omitir las $variables anteriores (lo que hace que sea más fácil de leer, en mi humilde opinión).

  5. Si movió la parte de verificación uniforme a una función separada, podría mejorar la legibilidad y la reutilización:

    function evencheck
    {
        return $(( $1 % 2 ))
    }
    function nevens
    {
        local -i sum=0
        for element
        do
            # `if` implicitly checks that the returned value/exit status is 0
            if evencheck "$element"
            then
                (( sum++ ))
            fi
        done
        echo "$sum"
    }

1
Si está buscando un uso de cuerpo terser loop sum=$((sum + 1 - element % 2)).
David Foerster

1
@DavidFoerster Terser y menos legible. ;-)
Konrad Rudolph

@DavidFoerster: Pensé que debería usar el resultado de mod-2 directamente. (O mejor, &1verificar el bit bajo si eso es más legible para usted). Pero podemos hacerlo más legible: sum=$((sum + !(element&1) ))usar un inverso booleano en lugar de +1 - condition. O simplemente cuente los elementos impares con ((odd += element&1)), y al final imprima con echo $(($# - element)), porque even = total - odd.
Peter Cordes el

Buen trabajo, y bueno para señalar pequeñas cosas que los principiantes se pierden, por ejemplo, local -iy sum++.
Paddy Landau

4

No estoy seguro si estás abierto a otras soluciones. Además, no sé si puede utilizar utilidades externas, o si está limitado exclusivamente a bash builtins. Si puede usar grep, por ejemplo, su función podría ser mucho más simple:

function nevens {
    printf "%s\n" "$@" | grep -c '[02468]$'
}

Esto coloca cada número entero de entrada en su propia línea y luego usa greppara contar las líneas que terminan en un dígito par.


Actualización: @PeterCordes señaló que incluso podemos hacer esto sin grep, solo bash puro, siempre que la lista de entrada contenga enteros bien formados (sin puntos decimales):

function nevens{
    evens=( ${@/%*[13579]/} )
    echo "${#evens[@]}"
}

Esto funciona creando una lista llamada evensal filtrar todas las probabilidades y luego devolver la longitud de esa lista.


Enfoque interesante Por supuesto, esto contará 3.0como un número par; la pregunta no era precisa sobre qué forma tomarían los números.
G-Man dice 'reinstalar a Mónica' el

@ G-Man ya que bash no admite aritmética de coma flotante, es seguro asumir números enteros
muru

Como la pregunta menciona números "pares" (e, implícitamente, "impares"), es algo seguro asumir que se trata de números enteros. No veo cómo es válido sacar conclusiones sobre la pregunta a partir de las capacidades y limitaciones de la herramienta que se espera que use el usuario. Recuerde: la pregunta dice que esta es una tarea, supongo que para la escuela, porque esto es demasiado trivial para ser de un trabajo. Los maestros de escuela inventan algunas cosas locas.
G-Man dice 'reinstalar a Mónica' el

2
Bash puede filtrar una lista por sí solo si podemos suponer que ningún elemento contiene espacios en blanco: expanda la lista arg con números impares reemplazados por la cadena vacía usando ${/%/}la @matriz para requerir una coincidencia al final de la cadena, dentro de un inicializador de matriz. Imprime el recuento. Como de una sola línea para definir y ejecutar: foo(){ evens=( ${@/%*[13579]/} ); echo "${#evens[@]} even numbers"; printf "%s\n" "${evens[@]}"; }; foo 135 212 325 3 6 3 4 5 9 7 2 12310. Incluye imprimir realmente la lista para la depuración. Impresiones 5 even numbers 212 6 4 2 12310(en líneas sep.)
Peter Cordes

1
Probablemente ZSH tenga algo para filtrar adecuadamente una lista, en lugar de depender de la división de palabras para reconstruir una nueva matriz (me sorprendió un poco que bash no lo hiciera; solo aplicaba cosas de expansión de cadena escalar a cada elemento). Para listas grandes, no me sorprendería si grepfuera realmente más rápido que bash. Hmm, me pregunto si hay una pregunta de codegolf donde podría publicar esa función bash: P
Peter Cordes
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.