3

I haven't been able to find any simple implementations for finding the median of an array. How can do do this is bash without reinventing the wheel?

If currently using this:

median() {
  arr=$1
  nel=${#arr[@]}
  if (( $nel % 2 == 1 )); then     # Odd number of elements
    val="${arr[ $(($nel/2)) ]}"
  else                            # Even number of elements
    val="$(( ( arr[$((nel/2))] + arr[$((nel/2-1))] ) / 2 ))"
  fi
  printf "%d\n" "$val"
}

For some reason I still can't figure out, it's returning incorrect values, and it seems overly complicated for something so simple. I feel like there has to be a way to do this in one line.

123
  • 8,733
  • 14
  • 57
  • 99

2 Answers2

4

I think you want something like this:

#!/bin/bash
median() {
  arr=($(printf '%d\n' "${@}" | sort -n))
  nel=${#arr[@]}
  if (( $nel % 2 == 1 )); then     # Odd number of elements
    val="${arr[ $(($nel/2)) ]}"
  else                            # Even number of elements
    (( j=nel/2 ))
    (( k=j-1 ))
    (( val=(${arr[j]} + ${arr[k]})/2 ))
  fi
  echo $val
}

median 1
median 2 50 1
median 1000 1 40 50

Sample Output

1
2
45
Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
  • To work with floats, change your `(( val=(${arr[j]} + ${arr[k]})/2 ))` line to `val=$(echo "scale=2;(${arr[j]}" + "${arr[k]})"/2|bc -l)` – jaygooby Apr 10 '19 at 10:34
0

This should work both for integral and fractional data:

#!/bin/bash

median() {
    declare -a data=("${!1}")
    IFS=$'\n' sorted_data=($(sort <<<"${data[*]}"))
    local num_elements=${#sorted_data[@]}
    if (( $num_elements % 2 == 1 )); then     # Odd number of elements
        ((middle=$num_elements/2))
        val="${sorted_data[ $(($num_elements/2)) ]}"
    else                            # Even number of elements
        ((before_middle=$num_elements/2 - 1))
        ((after_middle=$num_elements/2))
        val=$(echo "(${sorted_data[$before_middle]} + ${sorted_data[$after_middle]})/2" | bc -l)
    fi
    # remove trailing zeros
    echo $val | sed -r 's/\.([0-9]*[1-9])0*$/\.\1/; s/\.0*$//;'
}


median 1
median 2 50 1
median 1000 1 40 50
median 1.5 2.5
median 0.3 0.6 0.9

yields:

1
2
45
2
0.6
einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • You don't need the `sed` call. Just pass a `scale` value as part of your `bc` call; `echo "(1.1+2)/2"|bc -l` = `1.55000000000000000000` `echo "scale=2;(1.1+2)/2"|bc -l` = `1.55` – jaygooby Apr 10 '19 at 10:36
  • @jaygooby: Can you edit this change into the answer please? – einpoklum Apr 10 '19 at 11:32