Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

I have some data spread over time intervals, and I want to take some of those data within the time intervals. For example, I have data at times within 1..9, 11..19, etc., and I want to take data within 1-2, then 11-12, etc.

This will be part of a more complex bash script, in which I want to include this condition, with a if cycle, to isolate the times where I can catch the data.

I was thinking something like:

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

where t_initial and t_final are calculated separately, by the script itself.

I can not write this condition in bash syntax. I found some other solutions, but they seem extremely long and inconvenient, so I am here to ask simpler and more readable solutions.


It is important that the code works properly with floats. I am trying to fix the OP solution for this, but still can't find a way.

share|improve this question
    
I don't get it. Data should have a single timestamp, why do you have 2 variables t_initial and t_final? –  LatinSuD Jun 27 at 9:18
    
@LatinSuD, Because I am working in phase, more than times, and I have to get only same phase intervals between given time intervals :) –  Py-ser Jun 27 at 9:26
    
What happens if t_initial=1, t_final=3, begin1=2,fin1=4? Is that a match or not? –  LatinSuD Jun 27 at 9:27
    
@LatinSuD, no it is not. begin and fin are intended to be the extreme times of a given interval. if the times are not within the intervals, then the condition is not satisfied. –  Py-ser Jun 27 at 9:29
    
@Py-ser, you need to give us more detail. what is the input? what is the actual values of t_initial and t_final? what is the current output? what is your desired output? –  glenn jackman Jul 17 at 1:39

4 Answers 4

up vote 1 down vote accepted
+50

bash doesn't natively support a range comparison, nor floating-point numbers, so we have to do some of that ourselves. I'm also going to define a function, and use bc for floating-point calculation. Here is the end result and test suite:

# 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

The compareRanges function takes at least two arguments. The first is your t_initial, and the second is your t_final. It can take arbitrarily many other arguments in pairs after that, which are your begin1, fin1, begin2, fin2, in order.

The first test case compares the ranges in the comments on the question: 1-3 and 2-4.

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

So 1 is t_initial, 3 is t_final, 2 is begin1, and 4 is fin1.

When you want to use multiple ranges, you list them all out in pairs afterwards:

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

Here we test against 1-4, 2-6, and 3-9. In the while loop we look at each pair in turn and compare it against t_initial and t_final.

Because bash doesn't support fractional numbers we use bc, an arbitrary-precision calculator. Its input is given by the <<<"$t_initial >= $1" ... part: that feeds the string into standard input. $1 is the start of the range we're currently looking at in this iteration of the loop, and $2 is the end; we compare both the lower and upper bounds at once with &&. bc will output 1 when the comparisons are true, and 0 when one is false. We save the result in in_range, and the function succeeds (return 0) when both our tests were true.

Fractional numbers can just be specified with their ordinary decimal form:

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

bc will handle numbers with as many fractional digits as you want and with whatever magnitude you need.

At the end, if none of the boundary pairs matched, we fail (return 1). You can use the function as:

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

The test suite should print all "OK" when you run it.


Alternatively, other shells (such as zsh) do support fractional variable values. If you could run your script in one of those you could avoid the use of bc, although the comparison is still better off in a function. At least in zsh's case they are floats, so they're not necessarily accurate; bc will always be correct.

share|improve this answer

Not sure if you would like this solution. It has 2 features:

  • No external programs are required
  • It uses a function, so at least it hides the complexity of the comparisons

This is it:

#!/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
share|improve this answer
    
Thanks a lot! I have never used functions in bash, could you tell me the logic behind the separation of the comparison? how can it understand to compare between 1 and 3, and not between, i.e., 3 and 3, or 5 and 2 (in the example above)? –  Py-ser Jun 27 at 10:50
    
Separation is cosmetic only. The important thing is parameter number. They are processed in pairs. –  LatinSuD Jun 27 at 11:01
    
does this work with decimal numbers too? –  Py-ser Jul 10 at 3:01
    
@Py-ser Unfortunately not. I think that feature would require the use of an external program like bc. –  LatinSuD Jul 10 at 12:21
    
Nice answer. I get the feeling that both numbers need to be within the range, so the condition would need to be (( ($1 <= $t1 && $t1 <= $2) && ($1 <= $t2 && $t2 <= $2) )) –  glenn jackman Jul 17 at 1:44

The "11..19" thing you are asking about is called brace expansion.

You could use either eval {$t_initial..$t_final} ,

...or

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

share|improve this answer

Here are a couple of shameless rip-offs of LatinSuD’s answer that handle floating point.  You’ll notice that his answer boasts “No external programs are required.”  This one uses the calculator program, bc, as he suggested:

#!/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

This simply takes the if (( $t1 >= $1 && $t2 <= $2 )) test and sends it to bc, and then captures the output from bc.


Another approach is to normalize the numbers to integers by multiplying by a power of ten.  This requires that you have a maximum number of decimal digits.  For example, if no data point has more than three digits to the right of the decimal point, we can multiply everything by 1000.

#!/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

If the parameter to the normalize function is a simple integer (i.e., a number with no decimal point, e.g., 17) we can multiply by 1000 simply by appending 000, so 1717000.  If the parameter to normalize is a floating point number (i.e., it contains a decimal point, e.g., 42.5), we still append the 000, and then use sed to remove the decimal point and everything after the third digit.  The sed command s/\(.*\)\.\(...\).*/\1\2/ takes a string like abcdef.ghijkl and returns abcdefghi, so 42.542.500042500 (i.e., 42.5 × 1000).

It may be possible to do this string manipulation entirely in bash, without using sed.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.