We could use
library(purrr)
library(tidyr)
library(stringr)
map_dfc(names(ZX)[-1], ~ df1 %>%
select(all_of(.x)) %>%
extract(1, into = str_c(names(.), "_", 1:2),
"\\((\\d+),(\\d+)\\)", convert = TRUE)) %>%
bind_cols(ZX['PTNUM'], .)
-output
PTNUM AGE1_2_1 AGE1_2_2 AGE2_3_1 AGE2_3_2 AGE3_2_1 AGE3_2_2
1 12345 23 35 NA NA NA NA
2 12346 NA NA 34 40 45 50
3 12347 17 22 NA NA 38 45
Or another option is
ZX %>%
mutate(across(starts_with('AGE'),
~ read.csv(text = str_remove_all(.x, "\\(|\\)"),
header = FALSE, fill = TRUE))) %>%
unpack(where(is.data.frame), names_sep = "_")
-output
# A tibble: 3 × 7
PTNUM AGE1_2_V1 AGE1_2_V2 AGE2_3_V1 AGE2_3_V2 AGE3_2_V1 AGE3_2_V2
<int> <int> <int> <int> <int> <int> <int>
1 12345 23 35 NA NA NA NA
2 12346 NA NA 34 40 45 50
3 12347 17 22 NA NA 38 45
For the updated data
library(data.table)
ZX2 %>%
pivot_longer(cols = starts_with("AGE")) %>%
mutate(value = str_remove_all(value, "\\(|\\)")) %>%
separate_rows(value, sep = ",") %>%
group_by(PTNUM, name) %>%
mutate(rn = as.integer(gl(n(), 2, n()))) %>%
ungroup %>%
mutate(rn2 = rowid(PTNUM, name, rn)) %>%
unite(name, name, rn2) %>%
pivot_wider(names_from = name, values_from = value) %>%
select(-rn) %>%
group_by(PTNUM) %>%
mutate(across(everything(), ~ .x[order(!is.na(.x))])) %>%
ungroup
-output
# A tibble: 4 × 7
PTNUM AGE1_2_1 AGE1_2_2 AGE2_3_1 AGE3_2_1 AGE2_3_2 AGE3_2_2
<int> <chr> <chr> <chr> <chr> <chr> <chr>
1 12345 23 35 <NA> <NA> <NA> <NA>
2 12346 <NA> <NA> 23 <NA> 28 <NA>
3 12346 <NA> <NA> 34 45 44 50
4 12347 17 22 <NA> 38 <NA> 45
data
ZX <- structure(list(PTNUM = 12345:12347, AGE1_2 = c("(23,35)", NA,
"(17,22)"), AGE2_3 = c(NA, "(34,40)", NA), AGE3_2 = c(NA, "(45,50)",
"(38,45)")), class = "data.frame", row.names = c(NA, -3L))
ZX2 <- structure(list(PTNUM = 12345:12347, AGE1_2 = c("(23,35)", NA,
"(17,22)"), AGE2_3 = c(NA, "(23,28,34,44)", NA), AGE3_2 = c(NA,
"(45,50)", "(38,45)")), class = "data.frame", row.names = c(NA,
-3L))