3
\$\begingroup\$

I have created an inventory that works via QR codes. Briefly: the QR code codes for an email with the book/student checking it out. The email is downloaded into R using the gmailR package (code not shown). The info from the email is taken and added to a table which is compared to a master table (the inventory) and the changes are then made accordingly to update the master table.

The updating works by looking to see if the book is already IN or OUT and then simply flipping it to the opposite. And if it is being checked in the student and the date are erased (switched back to NA).

My original approach to updating the table was to use a function from the apply family but the problem I ran in to was that I do not want to change ALL rows in the master table but only the ones that need to be updated. I could not figure out a way to do this without using a for loop. Is there a way to write this code more efficiently, perhaps using apply, or via any other vectorized functions? Also, any other suggestions about my general design strategy/approach for this inventory would be appreciated.

(By the way, I know I should probably use the date class for the date column but don't worry about that for now).

## Sample data. 
# new = List of books to update, the date, and student name. 
# master = The inventory
new <- structure(list(book = structure(1:3, .Label = c("Almost moon", "Ava my story", "Catching fire"), class = "factor"), date = structure(c(1L, 1L, 1L), .Label = "8/23/15", class = "factor"), student = structure(1:3, .Label = c("John", "Mary", "Sue"), class = "factor")), .Names = c("book", "date", "student"), row.names = c(NA, -3L), class = "data.frame")
master <- structure(list(book = c("A trick I learned from dead men", "Almost moon", "Austin monthly july 2013", "Ava my story", "Becoming jane austen", "Bossypants", "Catching fire", "Cold mountain", "Comfort food", "Confessions of a jane austen addict"), author = c("Aldridge", "Sebold", "Various", "Gardner", "Spence", "Fey", "Collins", "Frazier", "Jacobs", "Rigler"), status = c("IN", "IN", "IN", "IN", "IN", "IN", "IN", "IN", "IN", "IN"), student = c(NA, NA, NA, NA, NA, NA, NA, NA, NA, NA), date = c(NA, NA, NA, NA, NA, NA, NA, NA, NA, NA)), .Names = c("book", "author", "status", "student", "date"), row.names = c(NA, 10L), class = "data.frame")

## Update inventory
if(sum(new[,1] %in% master$book) == length(new[,1])) {
    matches <- which(master$book %in% new[,1])
    for(i in matches) {
        if(master[i, 3] == "IN") {
            master[i, 3] <- "OUT"
            master[i, 4] <- as.character(new[new$book == master[i, 1], "student"])
            master[i, 5] <- as.character(new[new$book == master[i, 1], "date"])
        } else if(master[i, 3] == "OUT") {
            master[i, 3] <- "IN"
            master[i, 4] <- NA
            master[i, 5] <- NA
        }
    }
} else {
    stop("At least one book not found in database")
}
\$\endgroup\$
  • \$\begingroup\$ Well I've just realized that there is an all() function that returns true if all values in a logical vector are true. I guess that means that the condition for the if statement can be simplified to all(new[,1] %in% master$book). \$\endgroup\$ – syntonicC Aug 27 '15 at 18:47
1
\$\begingroup\$

You can indeed use vectorization. Create two vectors of indices (or booleans like I did) to identify the rows of the master that need to be updated. To find the corresponding row from the new, you can use the match function like I did.

## Update inventory
if (all(new$book %in% master$book)) {
   idx.map       <- match(master$book, new$book)
   is.match      <- master$book %in% new$book
   checking.out  <- is.match & master$status == "IN"
   checking.in   <- is.match & master$status == "OUT"
   master$status[checking.out]  <- "OUT"
   master$status[checking.in ]  <- "IN"
   master$student[checking.out] <- as.character(new$student[idx.map[checking.out]])
   master$student[checking.in ] <- NA
   master$date[checking.out]    <- as.character(new$date[idx.map[checking.out]])
   master$date[checking.in ]    <- NA
} else {
   stop("At least one book not found in database")
}

You will notice another improvement: everywhere I am referring to columns using their name, for example, master$student instead of their index like master[..., 3]. It makes the code more readable and easier to maintain (Imagine what would happen if suddenly a column was inserted at the beginning of master: you would have to modify all your indices...)

\$\endgroup\$
  • \$\begingroup\$ These are all excellent improvements, thank you. It never occurred to me to condense verbose code into variables (idx.map, is.match etc.) for use later - in fact I've never seen this done before but it's a great idea for readability. This code is much more straightforward than mine and easy to change if I want to add new features. \$\endgroup\$ – syntonicC Aug 28 '15 at 14:45

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.