R Solution for Excel Puzzles

[This article was first published on Numbers around us - Medium, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Puzzles no. 499–413

Puzzles

Author: ExcelBI

All files (xlsx with puzzle and R with solution) for each and every puzzle are available on my Github. Enjoy.

Puzzle #499

Interesting numbers are pretty often in scope of our puzzles. Today we have Lynch-Bell Numbers, and we need to find first 500 of them. Why they are special? Because of 4 conditions: none of digits repeat, there is no 0 in it, is at least 2 digits long and it is divisible by all of its digits. So many conditions to code as well. It is case we have pretty memory consuming calculation and it took code about 1.5 to 2 minutes to get 500 hundred of them. Look at it and at the end I will show another solution that is much faster.

Loading data and libraries

library(tidyverse)
library(readxl)
library(tictoc)

path = "Excel/499 Lynch Bell Numbers.xlsx"
test = read_excel(path, range = "A1:A501")

Transformation (with time measuring)

contains_zero <- function(number) {
  any(strsplit(number, "")[[1]] == "0")
}

has_unique_digits <- function(number) {
  digits <- strsplit(number, "")[[1]]
  length(unique(digits)) == length(digits)
}

is_self_dividing <- function(number) {
  digits <- as.numeric(strsplit(number, "")[[1]])
  all(digits != 0 & as.numeric(number) %% digits == 0)
}

get_numbers <- function(number_of_digits) {
  start <- 10^(number_of_digits - 1)
  end <- 10^number_of_digits - 1
  range <- as.character(start:end)
  
  result <- range %>%
    keep(~ !contains_zero(.x) && has_unique_digits(.x) && is_self_dividing(.x))
  
  return(tibble(number = result))
}

tic()
result = map_dfr(2:7, ~ get_numbers(.x)) %>% head(500)
toc()
# 119.52 sec elapsed

Validation

identical(as.numeric(result$number), test$`Answer Expected`)
#> [1] TRUE
Extra solution in Python: only 3 seconds to run

import pandas as pd
import time

path = "499 Lynch Bell Numbers.xlsx"
test = pd.read_excel(path, usecols="A").values.flatten().tolist()

def check_lynchbell(n):
    s = str(n)
    if "0" in s or len(set(s)) < len(s) or n <= 10: return False
    return all(n%int(d) == 0 for d in s)

start_time = time.time()
numbers = [k for k in range(9876543) if check_lynchbell(k)][:500]
end_time = time.time()

execution_time = end_time - start_time

print("Execution time:", execution_time, "seconds") # Execution time: 3.345 seconds
print(test == numbers) # TruePuzzle #500

Sometimes people are not calculating thing regularly, and then need to do much more job at once. And this is how we see this problem table in our task. Person responsible for it just noted total amount, but we need exact number for each point each day. Fortunatelly it is easier that it seems.

And guess what? We already did this task before. Because of some mistake we have this doubled. Solutions are ready here:
https://github.com/kgryczan/excelbi_puzzles/blob/main/Power%20Query/PQ_Challenge_194.R
https://github.com/kgryczan/excelbi_puzzles/blob/main/Power%20Query/PQ_194.py

PowerQuery Puzzle solved with R

Puzzle #501

In today’s matrix we have some hidden patterns, some numbers like golden threads are used couple of times in line. And we need to find them either horizontally or vertically. Let’s check it.

Load libraries and data

library(tidyverse)
library(readxl)

path = "Excel/501 Find Consecutives in Grid.xlsx"
input = read_excel(path, range = "B2:M10", col_names = FALSE) 
test  = read_excel(path, range = "O1:O6")

Transformation

i1 = as.matrix(input)
i2 = t(i1)

find_repeats_in_rows <- function(matrix) {
  unique(unlist(apply(matrix, 1, function(row) {
    row[which(diff(row) == 0)]
  })))
}

result = union(find_repeats_in_rows(i1),
               find_repeats_in_rows(i2)) %>%
  sort() %>%
  as_tibble() %>%
  setNames("Answer Expected")

Validation

identical(result, test)
# [1] True                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           1] TRUE                                                                                                                                                                                                                                                                                                                                                                          

Puzzle #502

Sometimes to construct something new we have to deconstruct something old. And we need to remove some digits to find maximal number that is cube, but is also contains digits from given numbers. But we can also remove some digit if it helps achieving goal. And as result we need to show this max cube and digits we are removing. Find out how to do it.

Loading libraries and data

library(tidyverse)
library(readxl)

path = "Excel/502 Remove Minimum Digits to Make a Cube.xlsx"
input = read_excel(path, range = "A2:A10")
test  = read_excel(path, range = "B2:C10")

Transformation

cube_number <- function(number) {
  str_nums <- str_split(as.character(number), "")[[1]]
  digits_combinations <- map(1:(length(str_nums) - 1), ~combn(str_nums, ., simplify = FALSE))
  all_combinations <- flatten(digits_combinations) %>% map_chr(~paste(.x, collapse = ""))
  
  cube <- keep(all_combinations, ~ round(as.numeric(.x)^(1/3))^3 == as.numeric(.x)) %>% 
    as.numeric() %>% 
    max(., na.rm = TRUE, finite = TRUE)
  
  if (!is.na(cube)) {
    digits_left <- setdiff(str_nums, str_split(as.character(cube), "")[[1]])
    unique_digits <- unique(digits_left)
    return(list(unique_digits, cube))
  } else {
    return(list("", ""))
  }
}

result <- input %>%
  mutate(result = map(Numbers, cube_number)) %>%
  mutate(`Removed Digits` = map_chr(result, ~ pluck(.x, 1) %>% sort(.) %>% paste(collapse = ", ")),
         `Cube Number` = map_chr(result, ~ pluck(.x, 2) %>% as.character)) %>%
  mutate(`Removed Digits` = if_else(nchar(Numbers) == str_count(`Removed Digits`, "\\d"), NA, `Removed Digits`),
         `Cube Number` = if_else(`Removed Digits` == "", NA, as.numeric(`Cube Number`))) %>%
  select(-c(result, Numbers))

Validation

all.equal(result, test, check.attributes = FALSE)
# [1] TRUE

Puzzle #503

What we have here? Probably records from American receipts. Why American, because in other (at least western) countries taxes are included in shelf prices, unlike in USA. So today we have amounts without taxes, we know that tax is 10% except some groceries. So we need to calculate how much each customer have to pay. Let’s do it.

Loading libraries and data

library(tidyverse)
library(readxl)

path = "Excel/503 Payments Calculations.xlsx"
input1 = read_excel(path, range = "A1:B9")
input2 = read_excel(path, range = "D1:D9") %>% pull()
test = read_excel(path, range = "F1:L9")
colnames(test)[1] = "Customer"

Transformation

result = input1 %>%
  separate_rows(`Purchase product (Tax not included)`, sep = ", ") %>%
  separate(`Purchase product (Tax not included)`, into = c("Product","Amount"), sep = ": ") %>%
  mutate(Amount = str_remove_all(Amount, ",") %>% as.numeric(),
         amount_with_tax = ifelse(Product %in% input2, Amount, Amount * 1.1)) %>%
  summarise(SUM = sum(Amount),
            AVERAGE = mean(Amount),
            MAX = max(Amount),
            MIN = min(Amount),
            COUNT = n() %>% as.numeric(),
            Payment = sum(amount_with_tax),
            .by = Customer)

Validation

all.equal(result, test)
# Column Payment has wrong value in test data

Puzzle #504

Finally some text manipulation. But pretty easy. Among list of all U.S. Presidents we need to find those whose initials are all the same.

Loading libraries and data

library(tidyverse)
library(readxl)

path = "Excel/504 US Presidents All First Chars Same.xlsx"
input = read_excel(path, range = "A1:A47")
test = read_excel(path, range = "B1:B5")

Transformation

result = input %>%
  filter(map(`US Presidents`,  ~ length(str_extract_all(., "[A-Z]") %>% 
                                          unlist() %>% unique())) == 1)

Validation

identical(result$`US Presidents`, test$`Answer Expected`)
# [1] TRUE

Puzzle #505

Sometimes our task is to build structure of numbers or letters. And today we are placing whole English alphabet on pyramid. As usual in such cases I prefer do it using matrices.

Loading libraries and data

library(dplyr)
library(purrr)

Transformation

create_triangular_dataframe <- function(letters) {
  n <- max(which((1:10 * (1:10 + 1)) / 2 <= length(letters)))
  num_rows <- n + 1
  num_cols <- n * 2 + 1
  
  df <- data.frame(matrix("", nrow = num_rows, ncol = num_cols))
  
  letters_idx <- 1
  
  fill_row <- function(row_num, num_letters) {
    start_col <- (num_cols - (2 * num_letters - 1)) %/% 2 + 1
    df[row_num, seq(start_col, by = 2, length.out = num_letters)] <<- letters[letters_idx:(letters_idx + num_letters - 1)]
    letters_idx <<- letters_idx + num_letters
  }
  
  walk(1:n, ~ fill_row(.x, .x))
  
  fill_row(num_rows, length(letters) - (n * (n + 1)) / 2)

  colnames(df) <- LETTERS[1:ncol(df)]  
  print(df, row.names = FALSE)
}

df = create_triangular_dataframe(LETTERS)

Puzzle #506

Imagine that 5 people assigned with letters A to E, are taking numbers from drawers. Some numbers are in ranges so we need to expand them, but then each person in order is placing its first number, then second number in next round and so on untill end of numbers. Doesn’t have sense in reality, but as excercise of mind it is pretty nice. Let’s do it.

Loading libraries and data

library(tidyverse)
library(readxl)

path = "Excel/506 Align Concated Alphabets & Numbers.xlsx"
input = read_excel(path, range = "A1:B6")
test = read_excel(path, range = "C1:C22")

Transformation

replace_range <- function(input) {
  input %>%
    str_split(", ") %>%
    unlist() %>%
    map_chr(~ if (str_detect(.x, "-")) {
      range <- str_split(.x, "-")[[1]] %>%
        as.numeric()
      paste(seq(range[1], range[2]), collapse = ", ")
    } else {
      .x
    }) %>%
    paste(collapse = ", ")
}

result = input %>%
  mutate(Numbers = map_chr(Numbers, replace_range) %>% str_split(., ", ")) %>%
  unnest_wider(Numbers, names_sep = "_") %>%
  pivot_longer(cols = starts_with("Numbers"), values_to = "Value", names_to = NULL,  cols_vary = "slowest") %>%
  filter(!is.na(Value)) %>%
  unite("Expected Answer", c("Alphabets", "Value"), sep = "") %>%
  select("Expected Answer")

Validation

identical(result$`Expected Answer`, test$`Expected Answer`)
# [1] TRUE  

Puzzle #507

Numbers are a part of language and culture. So today we have task to sort elements of dates spoken out as words, and then check if they are sorted alphabetically. Check it.

Loading libraries and data

library(tidyverse)
library(readxl)
library(english)

path = "Excel/507 Lexically Sorted MDY Dates.xlsx"
input = read_excel(path, range = "A1:A10")
test  = read_excel(path, range = "B1:B4")  

Transformation

result = input %>%
  mutate(parts = str_match(Dates, '(\\d{2})(\\d{2})(\\d{4})'),
         lit_month = month.name[as.integer(parts[,2])],
         lit_day = as.character(english(as.integer(parts[,3]))),
         lit_year = as.character(english(as.integer(parts[,4])))) %>%
  mutate(
    is_alphabetical = pmap_lgl(list(lit_month, lit_day, lit_year), 
                               ~ {
                                 lit_date <- c(..1, ..2, ..3)
                                 identical(lit_date, sort(lit_date))
                               })
  ) %>%
  filter(is_alphabetical) %>%
  select(`Expected Answer` = Dates)

Validation

identical(result, test)
# [1] TRUE

Puzzle #508

Again we have some number generation. Now we need numbers that are perfect square and square of its digits also sums up to perfect square. Those number are just oversquared 😀 But it is not that hard as it could. Although generating sequences can be time consuming, this time it is pretty fast. Check it out.

Loading libraries and data

library(tidyverse)
library(readxl)

path = "Excel/508 Number is Perfect Square and Sum of Squares of Digits is also a Perfect Square.xlsx"
test = read_excel(path, range = "A1:A501")

Transformation

is_perfect_square <- function(n) {
  sqrt_n <- sqrt(n)
  sqrt_n == floor(sqrt_n)
}

result <- integer(0)
i <- 1


while (length(result) < 500) {
  if (nchar(as.character(test[i, 1])) > 1) {
    if (is_perfect_square(test[i, 1]) && is_perfect_square(sum(as.numeric(strsplit(as.character(test[i, 1]), "")[[1]])^2))) {
      result <- c(result, test[i, 1])
    }
  }
  i <- i + 1
}

result = data.frame(Result =  unlist(result))

Validation

identical(result$Result, test$`Answer Expected`)
# [1] TRUE

Puzzle #509

If you are interested in computer science or math, I am almost sure that you know what Pascal’s Triangle is. But today we don’t need to simply generate such triangle, but also calculate sum of columns of numbers that are creating it. So we need to create two function for it. Look at it.

Loading libraries and data

library(tidyverse)
library(readxl)

path = "Excel/509 Pascal Triangle Column Sums.xlsx"
input = read_excel(path, range = "A1:A5")
test = read_excel(path, range = "B1:B5") %>%
  mutate(`Answer Expected` = ifelse(`Answer Expected` == "1, 1", "1, 1, 1", `Answer Expected`))

Transformation

generate_pascal_triangle = function(n) {
  triangle = matrix(0, n, 2*n - 1)
  triangle[1, n] = 1

  for (i in 2:n) {
    for (j in 1:(2*n - 1)) {
      if (j == 1) {
        triangle[i, j] <- triangle[i - 1, j + 1]
      } else if (j == 2*n - 1) {
        triangle[i, j] <- triangle[i - 1, j - 1]
      } else {
        triangle[i, j] <- triangle[i - 1, j - 1] + triangle[i - 1, j + 1]
      }
    }
  }
  return(triangle)
}

colsum_pascal_triangle = function(n) {
  triangle = generate_pascal_triangle(n)
  colsum = colSums(triangle) %>%
    paste(collapse = ", ")
  return(colsum)
}

result = input %>%
  mutate(`Answer Expected` = map_chr(Rows, colsum_pascal_triangle)) %>%
  select(`Answer Expected`)

Validation

identical(result, test)
# [1] TRUE

Puzzle #510

I like this task a lot. It is probably one of my favourite. We just need to split random string and count how many characters of each class are there. How many capital letters, how many digits, punctuators and specials and of course lower letters. Enjoy!

Loading libraries and data

library(tidyverse)
library(readxl)

path = "Excel/510 Find Upper, Lower, Numbers & Special Chars Count.xlsx"
input = read_excel(path, range = "A2:A12")
test  = read_excel(path, range = "B2:E12")

Transformation

result = input %>% 
  mutate(
    Data = ifelse(is.na(Data), "", Data),
    `Upper Case` = str_count(Data, "[A-Z]"),
    `Lower Case` = str_count(Data, "[a-z]"),
    Numbers = str_count(Data, "[0-9]"),
    `Special Chars` = str_count(Data, "[^A-Za-z0-9]")
  ) %>%
  select(-Data) %>%
  mutate(across(everything(), as.numeric))

Validation

identical(test, result)
# [1] TRUE

Puzzle # 511

Pig Latin is form of encrypting messages by cutting word in certain place, change the order of parts, and then adding “ay” at the end. But today we don’t have to code it, but to decrypt it. As many people spotted, reverse logic for Pig Latin is not best. Decrypted words can be different that encoded. So I find another way and decided to look for all possible real words that could be encoded. Find out my solution.

Loading libraries and data

library(tidyverse)
library(readxl)
library(hunspell)

path = "Excel/511 Pig Latin Decrypter.xlsx"
input = read_excel(path, range = "A1:A10")
test = read_excel(path, range = "B1:B10")

Transformation

rotate_word <- function(word, n) {
  len <- nchar(word)
  if (n >= len) return(word)
  substr(word, n + 1, len) %>% paste0(substr(word, 1, n))
}
decrypt_pig_latin <- function(sentence) {
  words <- str_split(sentence, " ")[[1]]
  decrypted_words <- words %>% map_chr(~ {
    word <- str_remove(.x, "ay[[:punct:]]?$")
    punctuation <- str_extract(.x, "[[:punct:]]$")
    len <- nchar(word)
    possible_words <- map_chr(0:(len - 1), ~ rotate_word(word, .x))
    valid_words <- possible_words %>% keep(hunspell_check)
    if (length(valid_words) > 0) {
      concatenated_valid_words <- paste(valid_words, collapse = "/")
      if (!is.na(punctuation)) {
        return(paste0(concatenated_valid_words, punctuation))
      } else {
        return(concatenated_valid_words)
      }
    } else {
      return(.x)
    }
  })
  paste(decrypted_words, collapse = " ")
}

result = input %>%
  mutate(`Answer Expected` = map_chr(`Encrypted Text`, decrypt_pig_latin)) %>%
  select(`Answer Expected`)

Validation

result == test
# Answer Expected
# [1,]            TRUE
# [2,]            TRUE
# [3,]            TRUE
# [4,]            TRUE
# [5,]            TRUE
# [6,]            TRUE
# [7,]            TRUE
# [8,]            FALSE who/how was your weekend?
# [9,]            TRUE

Puzzle #512

This task was one of the most suprising to me, and one when I first time used binary manipulations. We need to find first sparse number that is bigger than given number. What sparse means? That in its binary representations we do not find any pair, triplet or bigger set of adjacent 1s. I will provide you with 2 solutions, but first is only working for smaller numbers (it didn’t manage to make last to numbers in reasonable time) and second one using binary calculations which is very fast and don’t have problem with largest numbers.

Loading libraries and data

library(tidyverse)
library(readxl)

path = "Excel/512 Next Sparse Number.xlsx"
input = read_excel(path, range = "A1:A10")
test  = read_excel(path, range = "B1:B10")

Transformation — slow method

next_sparse_number_slow <- function(n) {
  is_sparse <- function(n) {
    binary_n <- intToBits(n)
    for (i in 2:length(binary_n)) {
      if (binary_n[i] == 1 && binary_n[i - 1] == 1) {
        return(FALSE)
      }
    }
    return(TRUE)
  }
  detect((n + 1):(2 * n + 1), is_sparse)
}

result = input %>%
  mutate(`Answer Expected` = map_dbl(Number, next_sparse_number_slow))

Transformation — fast binary method

next_sparse_number_fast <- function(n) {
  repeat {
    m <- n
    k <- 0
    change_required <- FALSE

    while (m > 0) {
      if ((m %% 4) == 3) {
        change_required <- TRUE
        break
      }
      m <- m %/% 2
      k <- k + 1
    }
    
    if (!change_required) break
    
    n <- (n %/% (2^(k + 1))) * (2^(k + 1)) + (2^(k + 1))
  }
  return(n)
}


result = input %>%
  mutate(`Answer Expected` = map_dbl(Number, next_sparse_number_fast))

# Code explanation for those not familiar with binary manipulations.
Initialize Variables:  
    Start with m as the input number n.
    Set k to 0, which keeps track of the bit position.
    Set a flag change_required to FALSE, indicating whether we need to modify the number.
Loop Until Sparse:
    We enter a loop that continues until we find a sparse number.
Check for Consecutive '1's:
    For each bit in m:
        If the last two bits are both '1' (checked using (m %% 4) == 3), it means the number isn't sparse. 
          We set change_required to TRUE and exit the inner loop.
        If not, move to the next bit by dividing m by 2 and incrementing k.
Adjust the Number if Needed:
    If change_required is TRUE (i.e., we found consecutive '1's):
    Calculate the new n by clearing bits from position k onwards and setting the next higher bit.
Return the Sparse Number:
    Once we have a number without consecutive '1's (change_required is FALSE), the loop stops, and we return the number n.

In Short:
The function checks each bit of the number to find if there are two '1's in a row. 
If it finds them, it changes the number to remove these consecutive '1's and tries again. 
This continues until the number is "sparse" (has no consecutive '1's), and then it returns that number.

Validation

identical(result$`Answer Expected`, test$`Answer Expected`)
#> [1] TRUE

Puzzle #513

And usually after having hard one, there is easy puzzle. Today we need to sort given number by their last digit, and if there was more than one with identical, go in order of original. Check it out.

Loading libraries and data

library(tidyverse)
library(readxl)
library(stringi)

path = "Excel/513 Sort by Unit Digit.xlsx"
input = read_excel(path, range = "A1:A10")
test  = read_excel(path, range = "B1:B10")

Transformation

result = input %>%
  arrange(str_sub(Numbers, -1)) 

Validation

identical(result$Numbers, test$`Answer Expected`)
# TRUE

Feel free to comment, share and contact me with advices, questions and your ideas how to improve anything. Contact me on Linkedin if you wish as well.
On my Github repo there are also solutions for the same puzzles in Python. Check it out!


R Solution for Excel Puzzles was originally published in Numbers around us on Medium, where people are continuing the conversation by highlighting and responding to this story.

To leave a comment for the author, please follow the link and comment on their blog: Numbers around us - Medium.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)