Objected Oriented Programming in R

R
OOP
Author

Jun Ryu

Published

March 9, 2023

We define a new object class pqnumber to handle computations with large floating point numbers.

A pqnumber object should be able to store large numbers and will be defined by the following four components:

  1. sign: \(1\) if positive, \(-1\) if negative
  2. p: number of digits after the decimal point
  3. q: (number of digits before the decimal point \(-\) \(1\))
  4. nums: vector of \(p + q + 1\) integers between \(0\) and \(9\),

For example, we can use the following object \(x\) to keep the number \(87654.321\):

x <- structure(list(sign = 1, p = 3, q = 4, nums = 1:8), class = "pqnumber")

a) Basics

To begin, we must start by defining the following five functions:

  • pqnumber(sign, p, q, nums): constructor function (returns a pqnumber object)
  • is_pqnumber(x): predicate function (returns TRUE if the input is a pqnumber object, FALSE otherwise)
  • print(x, DEC): print function (prints the components of the pqnumber object if DEC is FALSE, prints the decimal value otherwise)
  • as_pqnumber(x, p, q): generic coercion function from numeric to pqnumber
  • as_numeric(x): generic coercion function from pqnumber to numeric

To see how these functions are defined, click on the following:

Show the Code
pqnumber <- function(sign, p, q, nums) {
  # This function is the constructor function for a pqnumber
  # This will throw an error if:
  # 1) the sign argument is not the integer 1 or -1
  # 2) the p argument is not a whole number
  # 3) the q argument is not a whole number
  # 4) the length of the nums argument is not equal to p+q+1
  # 5) the entries in the nums vector are not numbers 0-9
  # Args:
  # sign: denotes the sign of the number
  # p: denotes how many digits are after the decimal point
  # q: denotes how many digits are before the units digit
  # nums: a numeric vector we use to construct the pqnumber
  # Return:
  # a pqnumber object
  
  if (abs(sign) != 1) {
    stop("The sign should be either 1 or -1.")
  }
  if (p %% 1 != 0 | q %% 1 != 0 | p < 0 | q < 0) {
    stop("p and q values must be whole numbers.")
  }
  if (length(nums) != p + q + 1) {
    stop("The length of nums must equal p+q+1.")
  }
  for (i in length(nums)) {
    if (!(nums[i] %in% 0:9)) {
      stop("The indices of nums must be an integer between 0 and 9.")
    } 
  }
  
  structure(list(sign = as.integer(sign), p = as.integer(p), q = as.integer(q), nums = as.integer(nums)), class = 'pqnumber')
}

is_pqnumber <- function(x) {
  # This function is the predicate function for a pqnumber
  # Args:
  # x: any input
  # Return:
  # a logical value checking whether the input is a pqnumber object or not
  any(class(x) == 'pqnumber')
}

print.pqnumber <- function(x, DEC = FALSE) {
  # This function is the print function for a pqnumber
  # Args:
  # x: a pqnumber
  # DEC: default set to FALSE, but if set to TRUE, will return the decimal form
  # Return:
  # either the decimal form or the object form (depends on the DEC input)
  if (DEC) {
    print_str <- paste0(substr(paste(rev(x$nums), collapse = ''), 1, x$q+1), '.', paste0(substr(paste(rev(x$nums), collapse = ''), x$q+2, length(x$nums))))
    
    if (x$sign == -1) {
      print_str <- paste0('-', print_str)
    }
  } else {
    print_str <- paste0('sign = ', x$sign, '\np = ', x$p, '\nq = ', x$q, '\nnums = ', paste(x$nums, collapse = ' '))
  }
  cat(paste0(print_str, '\n'))
}

as_pqnumber <- function(x, p, q) {
  # This function is the generic coercion function for a numeric value into a pqnumber
  # Args:
  # x: a numeric value
  # p: denotes how many digits are after the decimal point
  # q: denotes how many digits are before the units digit
  UseMethod('as_pqnumber')
}

as_pqnumber.numeric <- function(x, p, q) {
  # This function is the coercion method for the above generic function
  # Args:
  # x: a numeric value
  # p: denotes how many digits are after the decimal point
  # q: denotes how many digits are before the units digit
  # Return:
  # a pqnumber object that satisfies the given inputs
  nums <- (abs(x) * 10^seq(p, -q)) %% 10 %/% 1
  sgn <- if(x == 0) 1 else base::sign(x)
  pqnumber(sgn, p, q, nums)
}

as_numeric <- function(x) {
  # This function is the generic coercion function for a pqnumber into a numeric value
  # Args:
  # x: a pqnumber
  UseMethod('as_numeric')
}

as_numeric.pqnumber <- function(x) {
  # This function is the coercion method for the above generic function
  # Args:
  # x: a pqnumber
  # Return:
  # a numeric value that matches the pqnumber input
  if (x$sign == 1) {
    numeric <- as.numeric(paste(rev(x$nums), collapse = '')) / 10^(x$p)
  } else {
    numeric <- -1*(as.numeric(paste(rev(x$nums), collapse = '')) / 10^(x$p))
  }
  numeric
}

The following are tests to make sure our five functions work as intended:

Test Cases:

x <- pqnumber(1, 3, 4, 1:8)
x
sign = 1
p = 3
q = 4
nums = 1 2 3 4 5 6 7 8
y <- pqnumber(1, 6, 0, c(3,9,5,1,4,1,3))
y
sign = 1
p = 6
q = 0
nums = 3 9 5 1 4 1 3
z <- pqnumber(-1, 5, 1, c(2,8,2,8,1,7,2))
z
sign = -1
p = 5
q = 1
nums = 2 8 2 8 1 7 2
is_pqnumber(x) # expected value: TRUE
[1] TRUE
is_pqnumber(y) # expected value: TRUE
[1] TRUE
is_pqnumber(z) # expected value: TRUE
[1] TRUE
is_pqnumber(1230) # expected value: FALSE
[1] FALSE
print(x) # expect a pqnumber object (sign = 1, p = 3, q = 4, nums = 1:8)
sign = 1
p = 3
q = 4
nums = 1 2 3 4 5 6 7 8
print(y, DEC = T) # expected value: 3.141593
3.141593
print(z, DEC = T) # expected value: -27.18282
-27.18282
as_pqnumber(3.14, 3, 4) # expect a pqnumber object (sign = 1, p = 3, q = 4, nums = c(0,4,1,3,0,0,0,0))
sign = 1
p = 3
q = 4
nums = 0 4 1 3 0 0 0 0
as_pqnumber(-153.2772, 4, 2) # # expect a pqnumber object (sign = -1, p = 4, q = 2, nums = c(2,7,7,2,3,5,1))
sign = -1
p = 4
q = 2
nums = 2 7 7 2 3 5 1
as_numeric(x) # expected value: 87654.321 but might lose precision ("numeric" has a default printing of 7 digits)
[1] 87654.32
as_numeric(pqnumber(-1, 3, 6, rep(1,10))) # expected value: -1111111.111 but might lose precision
[1] -1111111

Great! All our outputs look correct. Now, the real question is: how can we define basic arithmetic functions for these pqnumber objects?

b) Addition and Subtraction

Our end goal in this section is to come up with two functions: add(x,y) and subtract(x,y) for \(2\) pqnumber objects x and y. To do this, let’s write two helper functions:

1. carry_over()

Algorithm:

  1. We iterate through each element of a numeric vector.

  2. We keep the remainder (when divided by \(10\)) in its element spot.

  3. We locate the existence of a carry by using %/% and seeing if it is not equal to \(0\).

  4. If it’s not equal to \(0\), we push the carry value onto the next element.

  5. We add the carried value to the next element and repeat the process from step 2.

Show the Code
carry_over <- function(z) {
  # This helper function is the carry over function that takes care of moving digits when it needs to be moved over to the next index
  # Args:
  # z: a numeric vector
  # Return:
  # a modified vector of z that has the carry over process completed
  n <- length(z)
  carry <- 0
  for (i in 1:n) {
    zi <- z[i] + carry
    z[i] <- zi %% 10
    carry <- zi %/% 10
  }
  
  if (carry != 0) {
    z[n+1] <- carry
  }
  z
}

Test Cases:

x <- c(13,4,5)
carry_over(x) # expected value: c(3,5,5)
[1] 3 5 5
y <- c(31,52,9) 
carry_over(y) # expected value: c(1,5,4,1)
[1] 1 5 4 1

2. abs_gtr()

Algorithm:

  1. First, we appropriately pad both numbers by adding extra \(0\)s at the end to either \(x\) or \(y\) if one has more digits after the decimal point.

  2. We do a similar padding by adding extra \(0\)s at the front to either \(x\) or \(y\) if one has more digits before the decimal point.

  3. Now, we should have \(x\) and \(y\) have equal lengths in their nums vectors.

  4. We reverse the nums vectors for both \(x\) and \(y\) and compare the digits starting from the first digit.

  5. By comparing the first digit, if \(x\)’s first digit is greater, return TRUE, if \(y\)’s is, then return FALSE.

  6. If the digits are the same, then we move on to the next digit and do the comparison again.

Show the Code
abs_gtr <- function(x, y) {
  # This helper function compares the absolute magnitudes of two pqnumbers
  # Args:
  # x: a pqnumber
  # y: a pqnumber
  # Return:
  # TRUE is x has a greater absolute magnitude than y, FALSE if otherwise
  if (x$p > y$p) {
    y <- pqnumber(y$sign, x$p, y$q, c(rep(0, x$p-y$p), y$nums))
  } else {
    x <- pqnumber(x$sign, y$p, x$q, c(rep(0, y$p-x$p), x$nums))
  }
  
  if (x$q > y$q) {
    y <- pqnumber(y$sign, y$p, x$q, c(y$nums, rep(0, x$q-y$q)))
  } else {
    x <- pqnumber(x$sign, x$p, y$q, c(x$nums, rep(0, y$q-x$q)))
  }
  
  x_rev <- rev(x$nums)
  y_rev <- rev(y$nums)
  gtr <- character(1)
  
  for (i in 1:length(x$nums)) {
    if (y_rev[i] > x_rev[i]) {
      gtr <- F
      break
    } else if (x_rev[i] > y_rev[i]) {
      gtr <- T
      break
    } else {
      next
    }
  }
  
  gtr
}

Test Cases:

x <- pqnumber(1,5,3,1:9) # 9876.54321
y <- pqnumber(1,2,0,c(4,1,3)) # 3.14
z <- pqnumber(-1,1,5,rep(3,7)) # -333333.3
w <- pqnumber(-1,6,3,c(2,1,1,2,4,5,3,8,7,7)) # -7783.542112

abs_gtr(x, y) # expected value: TRUE
[1] TRUE
abs_gtr(y, x) # expected value: FALSE
[1] FALSE
abs_gtr(x, z) # expected value: FALSE
[1] FALSE
abs_gtr(w, x) # expected value: FALSE
[1] FALSE

With the above helper functions, we are now ready to write the two main functions:

add()

Algorithm:

  1. We first initialize a vector \(z\) of length \(\text{max\_p} + \text{max\_q} + 1\), which represents the maximum number of digits we can possibly obtain by adding two numbers (considering the case of a carry-over).

  2. Now, we split it into casework: first case is when \(x\) and \(y\) have equal signs, second case is when \(x\) and \(y\) have unequal signs.

  3. When the signs are equal, we simply add the two by aligning positions, then call the carry-over helper function to carry over digits if necessary.

  4. When the signs are unequal, we first check which number has a greater absolute magnitude.

  5. Then, we grab the sign of the number that has a greater absolute magnitude and we change the signs for the nums vector of the other number.

  6. We proceed with the same addition process (as in step 3) and then use the carry-over function.

  7. We return the sum as a pqnumber object.

Show the Code
add <- function(x, y) {
  # This function adds two pqnumbers
  # Args:
  # x: a pqnumber
  # y: a pqnumber
  # Return:
  # the sum of x and y, returned in a pqnumber format
  max_p <- max(x$p, y$p)
  max_q <- max(x$q, y$q)
  n <- max_p + max_q + 1
  
  z <- rep(0L, n)
  if (x$sign == y$sign) {
    x_vals <- x$nums
    y_vals <- y$nums
    sgn <- x$sign
    
  } else {
    if(abs_gtr(x,y)) {
      x_vals <- x$nums
      y_vals <- -y$nums
      sgn <- x$sign
      
    } else {
      x_vals <- -x$nums
      y_vals <- y$nums
      sgn <- y$sign
    }
    
  }
  
  z[(1+max_p-x$p):(1+max_p+x$q)] <- x_vals
  z[(1+max_p-y$p):(1+max_p+y$q)] <- z[(1+max_p-y$p):(1+max_p+y$q)] + y_vals
  
  z <- carry_over(z)
  
  digit_offset <- length(z) - n
  pqnumber(sgn, max_p, max_q + digit_offset, z)
}

Test Cases:

x <- pqnumber(-1,3,4,1:8) # -87654.321
y <- pqnumber(-1,2,0,c(4,1,3)) # -3.14
z <- pqnumber(1,1,3,c(7,3,2,5,6)) # 6523.7
w <- pqnumber(1,3,5,c(3,1,2,4,5,3,8,7,7)) # 778354.213

add(x,z) # expected value: -81130.621 in pqnumber form
sign = -1
p = 3
q = 4
nums = 1 2 6 0 3 1 1 8
add(z,w) # expected value: 784877.913 in pqnumber form
sign = 1
p = 3
q = 5
nums = 3 1 9 7 7 8 4 8 7
add(z,y) # expected value: 6520.56 in pqnumber form
sign = 1
p = 2
q = 3
nums = 6 5 0 2 5 6

subtract()

Algorithm:

  1. We simply change the sign of \(y\) by multiplying its sign by \(-1\).

  2. Then, we do add(x,y) since \(x + (-y)\) is equal to \(x - y\) (or subtract(x,y)).

Show the Code
subtract <- function(x, y) {
  # This function subtracts one pqnumber from the other
  # Args:
  # x: a pqnumber
  # y: a pqnumber
  # Return:
  # the difference of x and y, returned in a pqnumber format
  y$sign <- y$sign * -1
  add(x, y)
}

Test Cases:

x <- pqnumber(-1,3,4,1:8) # -87654.321
y <- pqnumber(-1,2,0,c(4,1,3)) # -3.14
z <- pqnumber(1,1,3,c(7,3,2,5,6)) # 6523.7
w <- pqnumber(1,3,5,c(3,1,2,4,5,3,8,7,7)) # 778354.213

subtract(x,z) # expected value: -94178.021 in pqnumber form
sign = -1
p = 3
q = 4
nums = 1 2 0 8 7 1 4 9
subtract(w,x) # expected value: 866008.534 in pqnumber form
sign = 1
p = 3
q = 5
nums = 4 3 5 8 0 0 6 6 8
subtract(y,z) # expected value: -6526.84 in pqnumber form
sign = -1
p = 2
q = 3
nums = 4 8 6 2 5 6

Perhaps now we can write a different arithmetic function…?

c) Multiplication

We will try to define the product of two pqnumber objects.

multiply()

Algorithm:

  1. We first initialize a vector \(z\) of length \(x\$p + x\$q + y\$p + y\$q + 1\), which represents the maximum number of digits we can possibly obtain by multiplying two numbers (considering the case of a carry-over).

  2. Now, we iterate through each element of nums of \(y\) and we multiply it with the entire nums vector of \(x\) and place it in the appropriate indices of \(z\).

  3. We use carry-over function to clear any carries.

  4. We determine the sign of the product by multiplying the sign of \(x\) with the sign of \(y\).

  5. We return the product as a pqnumber object.

Show the Code
multiply <- function(x,y) {
  # This function multiplies two pqnumbers
  # Args:
  # x: a pqnumber
  # y: a pqnumber
  # Return:
  # the product of x and y, returned in a pqnumber format
  
  n <- x$p + x$q + y$p + y$q + 1
  z <- rep(0L, n)
  
  for (r in 1:(1+y$p+y$q)) {
    
    x_leftover <- x$p + x$q
    z[r:(r+x_leftover)] <- z[r:(r+x_leftover)] + (x$nums*y$nums[r])
  }
  z <- carry_over(z)
  digit_offset <- length(z) - n
  sgn <- x$sign * y$sign
  pqnumber(sgn, x$p + y$p, x$q + y$q + digit_offset, z)
}

Test Cases:

x <- pqnumber(-1,1,1,1:3) # -32.1
y <- pqnumber(-1,2,0,c(4,1,3)) # -3.14
z <- pqnumber(1,3,2,c(3,1,9,4,5,7)) # 754.913

multiply(x,z) # expected value: -24232.7073 in pqnumber form
sign = -1
p = 4
q = 4
nums = 3 7 0 7 2 3 2 4 2
multiply(x,y) # expected value: 100.794 in pqnumber form
sign = 1
p = 3
q = 2
nums = 4 9 7 0 0 1
multiply(y,x) # expected value: 100.794 in pqnumber form
sign = 1
p = 3
q = 2
nums = 4 9 7 0 0 1