2
\$\begingroup\$

I was looking for a way to navigate through a dataframe in R using just the console that displays every n lines. There are a few functions you can find at StackOverflow that attempt to answer this but none are quite what I want. So I made my own function but it has a lot of if statements. How can I refactor my code to make it more concise and clean?

Note: I chose "." and "/" for page up and page down because code completion in RStudio automatically turns "[" into "[ ]". I'm open to any alternative symbols.

Here is the code. It is called "less" but it is obviously missing most of the functions in the Unix version of less.

less <- function(x, n = 10) {
  begin = 1
  end = n

  repeat {
    print(
      x[begin:end, ]
    )

    input <- readline("Type . to scroll up, type / to scroll down: ")

    if(input == ".") {
      begin = begin - n
      end = end - n
    }

    if(input == "/") {
      begin = begin + n
      end = end + n
    }

    if(end >= nrow(x)) break

    if(end < 1) break
  }
}
\$\endgroup\$
2
\$\begingroup\$

Without changing the interface too much, your code could use the following changes. Note that I am fixing three bugs regarding corner cases:

  1. if your data has fewer than n rows to start with (see where I used min)
  2. if your data has a single column (see where I used drop = FALSE)
  3. make sure that the last page will always show

I avoided duplicating the statements where begin and end are reset, by creating an increment variable. The code computing the increment (of +n or -n) has been moved to its own function. One improvement I've made is that the function will call itself again via Recall() in case the user entered something other than the expected . or /, i.e. it will prompt the user again.

less <- function(x, n = 10) {
  begin <- 1
  end   <- n

  get_increment <- function() {
    input <- readline("Type . to scroll up, type / to scroll down: ")
    if (input == ".") -n else if (input == "/") +n else Recall()
  }

  repeat {
    print(x[begin:min(end, nrow(x)), , drop = FALSE])

    increment <- get_increment()
    begin     <- begin + increment
    end       <- end   + increment

    if(begin > nrow(x) | end < 1) break
  }
}

Now regarding your design in general, I feel it would be nice to pre-split your data.frame every n rows, and let the user know that there are for example 12 pages and that he is currently looking at page 4/12. In the prompt, also allow the user to jump to a specific page in addition to the next or previous page. Finally, make it easier to exit via a specific code like "q" rather than wait for the user to go beyond the first or last page. Here is my attempt:

less <- function(x, n = 10) {

   N <- nrow(x)
   if (N == 0) {
      message("input has no rows")
      return(NULL)
   }

   row_page_idx <- (seq_len(N) - 1) %/% n
   pages <- split(x, row_page_idx)
   num_pages <- length(pages)
   page_idx <- 1

   get_new_page_idx <- function() {
      input <- readline('Type a page number, ">" for next, "<" for previous, or "q" to quit: ')
      if (input == "q") return(0)
      if (input == ">") return(min(num_pages, page_idx + 1))
      if (input == "<") return(max(1, page_idx - 1))
      integer <- suppressWarnings(as.integer(input))
      if (!is.na(integer) & integer %in% 1:num_pages) return(integer)
      Recall() # if all fails
   }

   repeat {
      print(pages[[page_idx]])
      message("Page ", page_idx, " of ", num_pages)
      page_idx <- get_new_page_idx()
      if(page_idx == 0) break
   }
}
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ How does suppressWarnings work here? If I just type, for example, as.integer(^) I get an error. So if the input is an unexpected symbol, why doesn't that cause an error? \$\endgroup\$ – syntonicC Sep 16 '16 at 18:09
  • \$\begingroup\$ My input object has to be a string since it is the output of callingreadline(). So you probably meant to test as.integer("^"). See that it returns NA with a warning. suppressWarnings will remove that warning. \$\endgroup\$ – flodel Sep 16 '16 at 22:53
2
\$\begingroup\$

Reinventing the wheel

This kind of functionality already exists in the page function, for example if you run this on the console:

page(cars, method=c('print'))

This will show the cars object in a pager just like the less command in UNIX. You can scroll up and down using naturally the arrow keys, and it even supports a wide range of the same keyboard shortcuts to skip screens (space), half-screens (d, u), jump to beginning or end (g, G), and exit anytime (q).

Inside RStudio, page works a bit differently. It opens a new tab with all of the content at once. That might not be the way you want, indeed.

Bugs

Be careful with corner cases, as @flodel already pointed out. In fact there were so many bugs he missed one: the last page will never be shown, for example:

less(head(cars, n=20))

This will show only one page, with the first 10 items, if you try to scroll down with / the function exits without showing anything.

Alternative implementation

The start and end variables are modified independently, but they are tightly related: start should be the previous end + 1, and end should be the next start - 1. This relationship is not expressed directly in the code, which could lead to accidents. For example, you might change the code setting the value of start, and forget to make the similar change to end.

An alternative approach that would solve this could be pre-calculating the start values into a vector, and track a single index pointing to the current start value in this vector. That is, the start is the value at the index, and the end is the value at [index+1] - 1.

less <- function(x, n = 10) {
  intervals <- c(seq(1, nrow(x), by=n), nrow(x) + 1)
  index <- 1

  next_index <- function() {
    input <- readline("Type . to scroll up, type / to scroll down: ")
    if (input == ".") index - 1
    else if (input == "/") index + 1
    else Recall()
  }

  repeat {
    start <- intervals[index]
    end <- intervals[index + 1] - 1
    print(x[start:end, , drop = FALSE])

    if (length(intervals) -1 <= index) break

    index <- next_index()

    if (index < 1) break
  }
}

Usability

/ and . seems unusual as navigation shortcuts. How about d and u instead, for "down" and "up"? Or, though this might get too geeky, but many software use j and k for vertical navigation.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ This is an interesting lesson for me. The way I designed this was to look at specifically one type of dataframe that my data typically comes in and I wasn't thinking about passing other functions like head to it. My testcases were basically just the kind of dataframe/layouts that I needed at the time. I know books have probably been written on this subject but: How does one write a function properly to think of all the corner cases and things a user might try to do when there are so many options out there? \$\endgroup\$ – syntonicC Sep 16 '16 at 14:39
  • \$\begingroup\$ You are correct: In RStudio the page doesn't act the way I wanted. \$\endgroup\$ – syntonicC Sep 16 '16 at 14:40
  • 1
    \$\begingroup\$ How to think of all corner cases... I don't know, I just try to break stuff, it's fun :) The fact that I'm using head is irrelevant. I could have passed any data frame with 20 rows. I used 3 kinds of test cases, with row count less than n, equal to a multiple of n, or greater than n and not a multiple. Instead of creating multiple data frames, I used the stock cars data frame and head to control the row count. That's all. \$\endgroup\$ – janos Sep 16 '16 at 16:52
  • \$\begingroup\$ Alright, thanks for your help! I appreciate all your improvements, I've learned a lot from this. \$\endgroup\$ – syntonicC Sep 16 '16 at 18:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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