Polydivisible Numbers puzzle

Excel BI’s Excel Challenge #301 — solved in R

Numbers around us
Numbers around us
3 min readOct 11, 2023

--

In a recent LinkedIn post, ExcelBI threw down an interesting challenge: Find the Polydivisible numbers from a list. This caught my attention, and I decided to tackle this using R. In this post, I’ll walk you through how to solve this puzzle using three different R methodologies: base R, tidyverse, and data.table.

Defining the Puzzle:

Polydivisible numbers have a unique property:

  • The first two digits are divisible by 2.
  • The first three digits are divisible by 3.
  • … and so on.

For example, consider the number 9876:

  • 98 is divisible by 2.
  • 987 is divisible by 3.
  • 9876 is divisible by 4.

Our goal is to identify such numbers from a provided list.

Exercise File: For those who’d like to follow along or test their solutions, you can download the file from here.

Loading Data from Excel:

We’ll typically have the data in two segments — one with the numbers to check (input) and another with the expected answers (test). Let’s load them:

library(readxl)
library(tidyverse)
library(data.table)

path_to_file <- “path/Polydivisible Numbers.xlsx” # replace with correct path
input <- read_excel(path_to_file, range = "A1:A10")
test <- read_excel(path_to_file, range = "B1:B6")

Approach 1: Tidyverse with purrr

is_polydivisible_tv = function(number) {
digits = str_split(number, “”)[[1]]

map_lgl(1:length(digits), function(x)
{
num = as.numeric(paste0(digits[1:x], collapse = “”))
num %% x == 0
}) %>%
all()
}

result_tv = input %>%
mutate(check = map_lgl(Number, is_polydivisible_tv)) %>%
filter(check) %>%
select(`Expected Answer` = Number)

Approach 2: Base R

# Base R function
is_polydivisible_base <- function(number) {
digits <- strsplit(as.character(number), “”)[[1]]
checks <- sapply(1:length(digits), function(x) {
num <- as.numeric(paste(digits[1:x], collapse = “”))
return(num %% x == 0)
})
return(all(checks))
}

# Applying the function in Base R
check <- sapply(input$Number, is_polydivisible_base)
result_base <- data.frame(`Expected Answer` = input$Number[check])

Approach 3: Data.table

# Function using data.table
is_polydivisible_dt <- function(number) {
digits_str <- unlist(strsplit(as.character(number), “”))
digits_num <- as.numeric(digits_str)

dt <- data.table(position = 1:length(digits_num), digit = digits_num)

dt[, cum_num := as.numeric(paste0(digits_str[1:.I], collapse = “”)), by = .(position)]
dt[, is_divisible := cum_num %% position == 0]

return(all(dt$is_divisible))
}

# Applying the function with data.table
input_dt <- data.table(input)
input_dt[, check := lapply(Number, is_polydivisible_dt)]
result_dt <- input_dt[check == TRUE, .(Expected_Answer = Number)]

Validating Our Solutions:

It’s crucial to ensure that our solutions are accurate. For this, we’ll compare the results of each approach with the expected answers:

# For tidyverse approach
identical(result_tv$`Expected Answer`, test$`Expected Answer`)

[1] TRUE

# For base R approach
identical(result_base$Expected.Answer, test$`Expected Answer`)

[1] TRUE

# For data.table approach
identical(result_dt$`Expected Answer`, test$`Expected Answer`)

[1] TRUE

Discussion and Optimization:

Comparing the three approaches, you might notice:

  • Tidyverse and purrr provide a very readable syntax, especially for those familiar with functional programming.
  • Base R’s sapply is a classic method, handy and efficient for many.
  • Data.table shines in terms of performance, especially with larger datasets.

Performance Tip: For truly massive datasets, consider optimizing the is_polydivisible function to exit early when a non-divisibility is detected. This can save significant computation time.

I’d love to hear from you. Do you have a different way to tackle this puzzle? Maybe a faster approach or an optimization? Let’s discuss in the comments below!

--

--

Numbers around us
Numbers around us

Self developed analyst. BI Developer, R programmer. Delivers what you need, not what you asked for.