Homework 1

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.5.1     ✔ tibble    3.2.1
✔ lubridate 1.9.3     ✔ tidyr     1.3.1
✔ purrr     1.0.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors

Task 1 - Implement fizzbuzz

Write up

In order to write our fizzbuzz function, we took a series of steps. We began by thinking broadly about the errors we needed to throw before the actual function could run. We determined that the following would be necessary: checking for non-numeric types of inputs, negative values, inputs that are not coercible to integers without rounding or truncating, infinite values, and NAs and NaNs.

Once we came up with the criteria, we wrote the necessary code to check for this using stopifnot() statements and decided on helpful error messages to include. We then worked on setting up the format for the function. We used a for-loop to check each element of the vector and determine what it needs to be replaced with. We had to consider the order of the if else statements to ensure that each number was being labelled correctly.

Throughout this entire process, we were checking individual parts of our code using test cases in the console. Then we ran the good and bad test cases and discovered that we had too many double negatives in our stopifnot() statements, which we resolved. After debugging our code, we ran all of the test cases, and added our own, to ensure that our function was running properly.

Function

fizzbuzz = function(input) {
  n = length(input)
  return_vector = vector(mode = "character", length = n)
  
  #stop cases for the entire vector
  stopifnot("Input vector has non-numeric types" = is.numeric(input))
  stopifnot("Input vector has values less than 0" = (input >= 0))
  stopifnot("Input vector has values not coercible to integer type" = (input %% 1 == 0))
  stopifnot("Input vector has infinite values" = !is.infinite(input))
  stopifnot("Input vector has NA values" = !is.na(input))
  stopifnot("Input vector has NaN values" = !is.nan(input))
  
  for (integer in 1:n) {
    value = input[integer]
    
    if ((value %% 3 == 0) & (value %% 5 == 0)) {
      return_vector[integer] = "FizzBuzz"
    }
    else if (value %% 3 == 0) {
      return_vector[integer] = "Fizz"
    }
    else if (value %% 5 == 0) {
      return_vector[integer] = "Buzz"
    }
    
    #integer not divisible by either 3 or 5
    else {
      return_vector[integer] = as.character(value)
    }
  }
  return (return_vector)
}

Testing

Valid Inputs

stopifnot( fizzbuzz(1)  == "1"       )
stopifnot( fizzbuzz(3)  == "Fizz"    )
stopifnot( fizzbuzz(5)  == "Buzz"    )
stopifnot( fizzbuzz(15) == "FizzBuzz")

stopifnot(all( fizzbuzz(1:5) == c("1", "2", "Fizz", "4", "Buzz") ))

stopifnot(all( fizzbuzz(9:15) == c("Fizz", "Buzz", "11", "Fizz", "13", "14", "FizzBuzz") ))
stopifnot(all( fizzbuzz(15:9) == c("FizzBuzz", "14", "13", "Fizz", "11", "Buzz", "Fizz") ))

Bad Inputs

# Testing helper function, returns TRUE if expr throws an error FALSE otherwise.
throws_error = function(expr) {
  inherits(try(expr, silent = TRUE), "try-error")
}
stopifnot(throws_error( fizzbuzz(-1) ))
stopifnot(throws_error( fizzbuzz(-3) ))
stopifnot(throws_error( fizzbuzz(-5) ))

stopifnot(throws_error( fizzbuzz(TRUE) ))
stopifnot(throws_error( fizzbuzz(FALSE) ))

stopifnot(throws_error( fizzbuzz(Inf) ))
stopifnot(throws_error( fizzbuzz(-Inf) ))
stopifnot(throws_error( fizzbuzz(NaN) ))

stopifnot(throws_error( fizzbuzz("A") ))
stopifnot(throws_error( fizzbuzz(1.5) ))
stopifnot(throws_error( fizzbuzz(1i) ))
stopifnot(throws_error( fizzbuzz(4i) ))
#good input
stopifnot( fizzbuzz(c(2, 4L, 5.0)) == c("2", "4", "Buzz"))

#bad input
stopifnot(throws_error( fizzbuzz(c("Hello", 4) )))
stopifnot(throws_error( fizzbuzz(`+`)))

Task 2 - Re-Implement fizzbuzz

Write up

Similarly to the first fizzbuzz problem we thought through the errors to check through and how to order the function fizzbuzz_s3. To be most efficient we chose to make our default method an error statement that returns the class of the input that is not supported. Then we considered the two types that could be supported by our function, doubles and integers. Since some doubles, such as 3.1, are not valid for fizzbuzz we stopped the function if the input %% did not equal 0. If this case passed we coerced the doubles to integers and re-ran our function. By this point we have checked for all of the bad cases and coerced acceptable doubles to integers, so our final function fizzbuzz_s3.integer used much of the same code from question 1 to loop over the integers and replaced each component as indicated in the fizzbuzz directions. Lastly we added a couple of test cases to further check our code.

Function

#define s3 method fizzbuzz_s3
fizzbuzz_s3 = function(input) {
  UseMethod("fizzbuzz_s3")
}

#define default fizzbuzz_s3 implementation
fizzbuzz_s3.default = function(input){
  stop("Class ", class(input), " is not supported by fizzbuzz_s3.", call. =FALSE)
}

#define fizzbuzz_s3 for doubles
fizzbuzz_s3.double = function(input){
  stopifnot("Input vector has values not coercible to integer type" = (input %% 1 == 0))
  
  #coerce input to integer
  input = as.integer(input)
  
  #rerun fizzbuzz_s3 to go to integer case
  fizzbuzz_s3(input)
}

#define fizzbuzz_s3 for integers
fizzbuzz_s3.integer = function(input){
  n = length(input)
  return_vector = vector(mode = "character", length = n)
  
  stopifnot("Input vector has values less than 0" = (input >= 0))
  stopifnot("Input vector has infinite values" = !is.infinite(input))
  stopifnot("Input vector has NA values" = !is.na(input))
  stopifnot("Input vector has NaN values" = !is.nan(input))
  
  for (integer in 1:n) {
    value = input[integer]
    
    if ((value %% 3 == 0) & (value %% 5 == 0)) {
      return_vector[integer] = "FizzBuzz"
    }
    else if (value %% 3 == 0) {
      return_vector[integer] = "Fizz"
    }
    else if (value %% 5 == 0) {
      return_vector[integer] = "Buzz"
    }
    
    #integer not divisible by either 3 or 5
    else {
      return_vector[integer] = as.character(value)
    }
  }
  return (return_vector)

}

Testing

Valid Inputs

stopifnot( fizzbuzz_s3(1)  == "1"       )
stopifnot( fizzbuzz_s3(3)  == "Fizz"    )
stopifnot( fizzbuzz_s3(5)  == "Buzz"    )
stopifnot( fizzbuzz_s3(15) == "FizzBuzz")

stopifnot(all( fizzbuzz_s3(1:5) == c("1", "2", "Fizz", "4", "Buzz") ))

stopifnot(all( fizzbuzz_s3(9:15) == c("Fizz", "Buzz", "11", "Fizz", "13", "14", "FizzBuzz") ))
stopifnot(all( fizzbuzz_s3(15:9) == c("FizzBuzz", "14", "13", "Fizz", "11", "Buzz", "Fizz") ))

Bad Inputs

stopifnot(throws_error( fizzbuzz_s3(-1) ))
stopifnot(throws_error( fizzbuzz_s3(-3) ))
stopifnot(throws_error( fizzbuzz_s3(-5) ))

stopifnot(throws_error( fizzbuzz_s3(TRUE) ))
stopifnot(throws_error( fizzbuzz_s3(FALSE) ))

stopifnot(throws_error( fizzbuzz_s3(Inf) ))
stopifnot(throws_error( fizzbuzz_s3(-Inf) ))
stopifnot(throws_error( fizzbuzz_s3(NaN) ))

stopifnot(throws_error( fizzbuzz_s3("A") ))
stopifnot(throws_error( fizzbuzz_s3(1.5) ))
stopifnot(throws_error( fizzbuzz_s3(1i) ))
stopifnot(throws_error( fizzbuzz_s3(4i) ))
#good input
stopifnot(fizzbuzz_s3(c(6L, 10.0, 11, 1L, 15.0)) == c("Fizz", "Buzz", "11", "1", "FizzBuzz"))

#bad input
stopifnot(throws_error(fizzbuzz_s3(c(2.675, 11.01, "abc", 4, "FizzBuzz"))))
stopifnot(throws_error(fizzbuzz_s3(`!`, `123`)))

Task 3 - Printing Secrets

Write up

When writing the print method for our new class secret we were faced with six mutually exclusive inputs. The input could be not an atomic vector, or one of the following atomic vectors, complex, raw, numeric, logical and character. Our first step was to stop the function and produce an error if the input was not atomic or was or type raw. Then we created a helper function for if the input was of type character or numeric. When constructing our numeric helper function we made sure to keep in mind the different output for a positive or negative number. Within the print.secret method we used a for loop for the logical atomic vectors to return the correct output and called our helper functions for atomic vectors of numeric (double and integer) and character type. Lastly, for atomic vectors of complex type, we split up the real and imaginary portions and leveraged our numeric helper function to construct the correct output. We also distinguished between negative and positive imaginary numbers to follow the rules as indicated in the examples. As a final step we used the noquote() function when necessary so that our output matched exactly with the examples. After we were finished we tested our code and added some additional cases.

Function

# use this function to create a secret object
secret = function(x) {
  structure(
    x, class="secret"
  )
}
# helper function for numeric inputs
numeric_secret = function(value) {
    is_negative = FALSE
    if (value < 0) {
      is_negative = TRUE
    }
    num_stars = nchar(as.character(value))
    
    # negative input, add "-"
    if (is_negative) {
      value_secret = paste0("-", paste(rep("*", num_stars - 1), collapse = ''))
      return (value_secret)
    }
    
    #non-negative input
    else{
      value_secret = paste(rep("*", num_stars), collapse = '')
      return (value_secret)
    }
}
# helper function for character inputs
char_secret = function(value){
  return(paste0("\"", paste(rep("*", nchar(value)), collapse = ""), "\""))
}
print.secret = function(x) {
  
  # check input conditions are met
  stopifnot("Method only works for atomic vectors of type character, integer, double, logical, or complex" = all(is.atomic(x)) & !all(is.raw(x)))
  
  # create empty return vector
  return_vector = vector(length = length(x))
  
  # check if logical, encode values
  if (is.logical(x)) {
    for (i in seq_along(x)) {
      return_vector[i] = "*****"
    }
    return_vector = noquote(return_vector)
  }

  # check if numeric, pass to numeric_secret()
  if (is.numeric(x)) {
    for (i in seq_along(x)) {
      return_vector[i] = noquote(numeric_secret(x[i]))
    }
  }
  
  # check if complex, pass to numeric_secret()
  if (is.complex(x)) {
    for (i in seq_along(x)) {
      
      # split into real and imaginary
      real = numeric_secret(Re(x[i]))
      imaginary = numeric_secret(Im(x[i]))
      if (Im(x[i]) < 0) {
        return_vector[i] = noquote(paste0(real, imaginary, "i"))
      }
      else {
        return_vector[i] = noquote(paste0(real, "+", imaginary, "i"))
      }
    }
  }
  
  # check if character, pass to char_secret()
   if (is.character(x)) {
    for (i in seq_along(x)) {
      return_vector[i] = char_secret(x[i])
    }
   }
  
  # print return_vector and return invisible x
  print(noquote(return_vector))
  return(invisible(x))
}

Testing

# helper function for creating, printing, and capturing output of a secret
# returns a character vector of the output
print_secret = function(x) {
  trimws( gsub('\\s+',' ',    
    capture.output(
      print(secret(x))
    )
  ) )
}

Valid Inputs

stopifnot( print_secret( 1:5 )                      == '[1] * * * * *'            )
stopifnot( print_secret( -(1:5) )                   == '[1] -* -* -* -* -*'       )
stopifnot( print_secret( c(123, 456, 789) )         == '[1] *** *** ***'          )
stopifnot( print_secret( c(123.456, -789) )         == '[1] ******* -***'         )
stopifnot( print_secret( c(-123.456, 789) )         == '[1] -******* ***'         )
stopifnot( print_secret( c(123+1234i, -123+1234i) ) == '[1] ***+****i -***+****i' )
stopifnot( print_secret( c(123-1234i, -123-1234i) ) == '[1] ***-****i -***-****i')
stopifnot( print_secret( c(TRUE, FALSE) )           == '[1] ***** *****'          )
stopifnot( print_secret( c(FALSE, TRUE) )           == '[1] ***** *****'          )
stopifnot( print_secret( c("abc", "def") )          == '[1] "***" "***"'          )

Bad Inputs

stopifnot( throws_error( 
  print_secret( list() ) 
) )

stopifnot( throws_error( 
  print_secret( raw() ) 
) )

stopifnot( throws_error( 
  print_secret( function() {} ) 
) )
#good input
stopifnot( print_secret( c("diff", "length") )      == '[1] "****" "******"')
stopifnot( print_secret( c(0 + 1i) ) == '[1] *+*i')

#bad input
bad_check <- list("hello", 123, 123 + 456i)

stopifnot( throws_error( 
  print_secret( bad_check )
) )