はじめに

文字列から部分文字列を抽出するとき、特定の文字(区切り文字)より前もしくは後を取り出したいことがあります。たとえば日付と時刻とがパイプ文字で区切られた下の文字列

teststr <- "April 29, 2023 | 8:45 AM - 9:30 AM"

から、日付部分(April 29, 2023)や時刻部分(8:45 AM - 9:30 AM)を抜き出すことです。

Excelでの慣用表現は、下のようにLEFT/RIGHT関数とFIND関数とを組み合わせます。

=LEFT(番地, FIND("|",番地) -1)
=RIGHT(番地, LEN(番地) - FIND("|", 番地) -1)

これをstringrパッケージで実現することを考えましょう。

直接抽出する方法

文字列の抽出ですから、直感的にはstr_extract()が浮かびます。区切り文字そのものを含むのではなく、その前後を抽出するには、正規表現の先読み(Lookahead)・後読み(Lookbehind)を使います。詳しくは、「Perl Hackers Hub No.58 正規表現の勘所――わかりづらい記法の覚え方、先読みや後読みの実践(3)」を参照してください。

# ExcelのLEFT+FINDに相当
teststr |> 
  str_extract("^(.*)(?= \\|)")
## [1] "April 29, 2023"
# ExcelのRIGHT+LEN+FINDに相当
teststr |> 
  str_extract("(?<=\\| )(.*)$")
## [1] "8:45 AM - 9:30 AM"

私は長らくこの方法しか知らなかったのですが、構文糖の多いtidyverseらしくなく、あまりしっくり来ていませんでした。

分割抽出する方法

そこでstr_split()を使って文字列を分割し、そのうえでN番目の要素を取得することを考えます。ところがstringrの文書を読んでみると、str_split_i()という、まさにそのための関数が最初から存在していることに気づかされました。

# ExcelのLEFT+FINDに相当
teststr |> 
  str_split_i(" \\| ", i = 1)
## [1] "April 29, 2023"
# ExcelのRIGHT+LEN+FINDに相当
teststr |> 
  str_split_i(" \\| ", i = -1)
## [1] "8:45 AM - 9:30 AM"

i = -1のように負の値をとっているときは、右から1番目であることを意味します。

str_extract()よりも統一的に書けるし他人にも説明しやすいので、これからは常用したいと思います。

おまけ(列ごと分割する)

ちなみに、その文字列がデータフレームの列として取り込まれているのなら、 tidyrパッケージを使ったほうが簡便かもしれません。たとえば

df_test <- tribble(~date_time, teststr) 
df_test
## # A tibble: 1 × 1
##   date_time                         
##   <chr>                             
## 1 April 29, 2023 | 8:45 AM - 9:30 AM

のように、パイプ文字で区切られたdate_time列を日付と時刻との2列に分割したければ、 separate_wider_delim()関数を使います。

df_test |> 
  separate_wider_delim(date_time, delim = " | ", names = c("date", "time"))
## # A tibble: 1 × 2
##   date           time             
##   <chr>          <chr>            
## 1 April 29, 2023 8:45 AM - 9:30 AM

separate_wider_delim()はまだ新しい(Experimentalな)関数なので、 separate()を使う人も多いはずです。

df_test |> 
  separate(date_time, into = c("date", "time"), sep = " \\| ")
## # A tibble: 1 × 2
##   date           time             
##   <chr>          <chr>            
## 1 April 29, 2023 8:45 AM - 9:30 AM