4

How to break a loop after a certain elapsed time? I have a function that collects observational data from a user. The user should have a pre-defined time limit, when the data are recorded (30 sec in the example). At the moment, the function breaks, if the user-input arrives later than the end of the time limit.

record.events <- function(duration = 30, first.event = "a"){
    # Initial settings
    time.start <- proc.time()[3]
    events <- c(first.event, 0)

    # Timed data collection
    while(proc.time()[3] - time.start < duration){
        temp <- readline("record events...")
        events <- c(events, temp, proc.time()[3] - time.start)
    }

    # Format recorded data for post-processing
    events <- as.data.frame(matrix(events, byrow=T, ncol=2, 
        dimnames=list(NULL, c("events","stroke.time"))))
    events[,2] <- round(as.numeric(as.character(events[,2])),3)
    return(events)
}

Gives for example this result:

  events stroke.time
1      a       0.000
2      f       2.618
3      a      23.791
4      f      24.781
5      a      33.488

The last event (a) arrived after the time limit. SO has a solution for this in matlab. Is there a way in R, how to stop waiting for the user input as soon as the time is up?

Edit:

While functions setTimeLimit() and R.utils::withTimeout() can terminate execution of a function that takes too long (thanks to Kodl, Carl Witthoft and Colombo, together with this answer), neither can interrupt readline(). Documentation to withTimeout specifies:

Furthermore, it is not possible to interrupt/break out of a "readline" prompt (e.g. readline() and readLines()) using timeouts; the timeout exception will not be thrown until after the user completes the prompt (i.e. after pressing ENTER).

The user input after the time limit is thus the only way, how to stop waiting for readline. The check can be executed with the while loop as in my code, or with setTimeLimit or withTimeout in a combination with tryCatch. I therefore accept Kodl's answer.

Community
  • 1
  • 1
nya
  • 2,138
  • 15
  • 29
  • What is the exact logical flow you want? Reject data after timeout? Issue a prompt and let the user decide whether to continue by extending the timeout? Something else? – Carl Witthoft May 19 '16 at 12:53
  • After the timeout, I would like to terminate the `while` loop, proceed with data formatting and return the events dataframe. – nya May 19 '16 at 13:01
  • I have no problem getting a `while` loop to terminate, using your conditional expression there. Can you verify that each line inside your `while` loop runs to completion on its own? – Carl Witthoft May 19 '16 at 15:01
  • It terminates correctly when the `readline` input arrives after the time limit. I wished to break out of the `while` at the time of the set limit. – nya May 20 '16 at 06:49
  • The only way to exit "at" the time limit is to check the current time at a rate 5x the amount of accuracy at which you want to determine "time exceeded". There's no magic way to predict time. – Carl Witthoft May 20 '16 at 11:11

3 Answers3

3

I've also been looking for a solution to this, ended up writing my own function below, but it only works in some setups/platforms. The main problem is that readline suspends execution until input is provided, so it may hang indefinitely, without ever returning.

My workaround is to open(file('stdin'), blocking=FALSE), and then use readLines(n=1). Problem with that is that it only accepts file('stdin'), which is not always connected. It fails in RGui for windows and MacOS, and for RStudio (at least for MacOS). But it seems to work for R when run from terminal under MacOS.

readline_time <- function(prompt, timeout = 3600, precision=.1) {
  stopifnot(length(prompt)<=1, is.numeric(timeout), length(timeout)==1, !is.na(timeout), timeout>=0, is.numeric(precision), length(precision)==1, !is.na(precision), precision>0)
  if(!interactive()) return(NULL)
  if(timeout==0) return(readline(prompt))
  my_in <- file('stdin')
  open(my_in, blocking=FALSE)
  cat(prompt)
  ans <- readLines(my_in, n=1)
  while(timeout>0 && !length(ans)) {
    Sys.sleep(precision)
    timeout <- timeout-precision
    ans <- readLines(my_in, n=1)
  }
  close(my_in)
  return(ans)
}

Or if you want to import (along with some other functions): devtools::install_github('EmilBode/EmilMisc')

Emil Bode
  • 1,784
  • 8
  • 16
1

i think you can use fucntion "setTimeLimit" from library base. so...

record.events <- function(duration = 30, first.event = "a"){
    # Initial settings
    time.start <- proc.time()[3]
    events<-first.event
    stroke.time<-c(0)

# Timed data collection
    while(proc.time()[3] - time.start < duration){
    temp <- tryCatch({setTimeLimit(elapsed=(time.start + duration - proc.time()[3]),
                     transient = TRUE);readline("record events...")},
                     error = function(e) { return("NULL")})
    #you need to set up back this function... (but why i dont know????)
    setTimeLimit(elapsed = Inf, transient = TRUE)

     events[length(events)+1] <- temp
    stroke.time[length(stroke.time)+1]<-round(proc.time()[3],3)
}

# Format recorded data for post-processing

events<-data.frame(events, stroke.time) 
return(events)
}

But setTimeLimit inst great for use in user functions.. My results is:

  events stroke.time
1      a        0.00
2      s     1539.12
3      s     1539.52
4    ass     1539.96
5      s     1540.49
6    asd     1540.94
7    fed     1541.27
8   NULL     1541.55

For more info see:

https://stackoverflow.com/a/7891479

https://stat.ethz.ch/R-manual/R-devel/library/base/html/setTimeLimit.html

How does setTimeLimit work in R?

setTimeLimit fails to terminate idle call in R

Community
  • 1
  • 1
Kodl
  • 26
  • 2
  • This is not working as needed. Updating the line stroke.time[length(stroke.time)+1]<-round(proc.time()[3]-time.start,3) to show stroke.time in elapsed seconds, still waits for the user indefinitely after timeout. As if `while` had precedence. – nya May 19 '16 at 12:34
  • Im not sure what you need. But when this fucntion ends in last redcord with "NULL" data, thats mean it ends in waiting for record. Isnt it what you want? – Kodl May 19 '16 at 14:05
  • 1
    The condition in the `while()` has the same effect, hasn't it? They both wait until after the time limit. I wished to leave the loop **at** the time limit. – nya May 20 '16 at 07:51
1

I was curious to see if anyone had a real Rland solution to this problem, but it looks like not.

One possible solution is to shell out with system() and run a command that allows reading input with a time limit. This is inherently platform-specific. The Unix bash shell provides a read builtin that is perfect for this purpose, and this will also work on the Cygwin emulation layer on Windows. Unfortunately, I haven't ever come across a command available on the vanilla native Windows platform that provides sufficient functionality for this. set /p can read arbitrary string input but does not provide a timeout, while choice.exe provides a timeout (accurate to the second) but only supports selection of an item from a finite list of (single-character!) items, as opposed to arbitrary string input. Fun fact: choice.exe has its own Wikipedia article.

Here's how you can use read to do this:

LIMIT <- 10; ## conceptual constant
end <- Sys.time()+(left <- LIMIT); ## precompute end of input window and init left
repeat {
    input <- suppressWarnings(system(intern=T,sprintf(
        'read -r -t %.2f; rc=$?; echo "$REPLY"; exit $rc;',
        left
    ))); ## suppress warnings on non-zero return codes
    left <- difftime(end,Sys.time(),units='secs');
    cat(sprintf('got input: \"%s\" [%d] with %.2fs left\n',
        input,
        if ('status'%in%names(attributes(input))) attr(input,'status') else 0L,
        left
    ));
    if (left<=0) break;
};
## asdf
## got input: "asdf" [0] with 9.04s left
## xcv
## got input: "xcv" [0] with 8.15s left
## a
## got input: "a" [0] with 6.89s left
## b
## got input: "b" [0] with 6.68s left
## c
## got input: "c" [0] with 6.44s left
##
## got input: "" [0] with 5.88s left
##
## got input: "" [1] with 4.59s left
## got input: "" [1] with 3.70s left
##
## got input: "" [0] with 0.86s left
##
## got input: "" [0] with 0.15s left
## got input: "" [142] with -0.03s left

The sample output I've shown above was me playing around during the input window. I mostly typed some random lines and pressed enter to submit them, giving a return code of 0. The two lines of output that show a return code of 1 were me pressing ^d, which causes read to return 1 immediately, leaving whatever input that was in the buffer in $REPLY (nothing, in those two cases). The final line of output was read terminating immediately upon hitting the timeout, which I believe is the functionality you're looking for. You can use the return code of 142 to distinguish the timeout event from other input events. I'm not completely certain that the return code of 142 is consistent and reliable on all Unix systems, but there's also another way to detect the timeout event: check the current time against end (i.e. the left calculation), as I do in the code. Although I suppose that approach introduces a race condition between a possible last-moment submission and the time check in Rland, but you probably don't need that level of design criticality.

bgoldst
  • 34,190
  • 6
  • 38
  • 64
  • This is way above my abilities to understand and modify by myself. Working with OS 10.11, I get 4 error messages per 0.01s reading `sh: line 0: read: 0.00: invalid timeout specification` and the same number of output lines `got input: "" [1] with -0.00s left` both from shell and from within R. While `%.2f` returns remaining time in `cat`, in `read -t` the same specification is not a number? – nya Jun 01 '16 at 13:03
  • Changing left to `numeric` did not help. Typing anything while the script runs does not get picked up into input either. I'm sorry to say your solution is not working for me. – nya Jun 01 '16 at 13:16
  • Unfortunately, it looks like the `read` builtin on Mac OS X may not accept fractional numbers, although on my system the help output for `read` says `TIMEOUT may be a fractional number.`. To work around this you would have to coerce `left` to integer with `as.integer()` just before passing it to the `sprintf()` call, and you'd have to change the format specification to `%d`. – bgoldst Jun 01 '16 at 14:29