Rのソートはデフォルトだとコードポイント順ではありません。何やら空気を読んでくれているようです。
## データの準備
d <- data.frame(
kana = c("お", "エ", "う", "イ", "あ"),
letter = c("A", "e", "C", "b", "Z"),
stringsAsFactors = FALSE
)
## 空気を読んだソートをしてくれる
sort(d$kana)
# > [1] "あ" "イ" "う" "エ" "お"
?Comparison
(R: Relational Operators)を確認すると書いてありますが、文字列同士の比較はデフォルトだとlocaleに依存した辞書順でソートされます。つまり、今の例のように日本語ロケールではカタカナとひらがなの違いは吸収されるわけです。
この挙動は分かって使う分には良いのですが、空気を読んでほしくない場合もあります。具体的に言えばコードポイント順にソートしてほしい場合もあるわけです。それにはいくつかやり方があります。
localeを変更する
素朴な方法としてSys.setlocale()
でlocaleを変更してしまうというやり方があります。しかし、locale = "C"
のままだと表示に難があります。
Sys.setlocale(locale = "C")
sort(d$kana)
# > [1] "\343\201\202" "\343\201\206" "\343\201\212" "\343\202\244"
# > [5] "\343\202\250"
ソートしてからlocaleを戻してやれば良いわけですが、面倒です。
sorted <- sort(d$kana)
Sys.setlocale(locale = "ja_JP")
sorted
# > [1] "あ" "う" "お" "イ" "エ"
withrを使う
とかなんとかボヤいていたら @yutannihilation さんが教えてくれました。ありがたや。
stringi::stri_sort(locale = “C”)とかどうでしょう。 https://t.co/ak8VAPw5n5 あとは一時的にロケールを変更したいならwithr::with_locale()とか。
— Hiroaki Yutani (@yutannihilation) December 4, 2018
これなら既存のロケールに影響することはありませんし、printはもとのロケールを反映するので表示にも問題ありません。
withr::with_locale(c("LC_COLLATE" = "C"), sort(d$kana))
# > [1] "あ" "う" "お" "イ" "エ"
※追記: ソートを変更するだけならwhth_collate()
で良さそうです。
withr::with_collate("C", sort(d$kana))
# > [1] "あ" "う" "お" "イ" "エ"
stringiについて
ところで、stringi
の方は日本語だとうまくいきませんでした。
stringi::stri_sort(d$kana, locale = "C")
# > [1] "あ" "イ" "う" "エ" "お"
locale = "C"
の指定が通っていないというわけではなく、アルファベットだとそれっぽく動きます。
## 通常の空気読んだソート
stringi::stri_sort(d$letter, locale = "en_EN")
# > [1] "A" "b" "C" "e" "Z"
## locale = "C"っぽさがあるソート
stringi::stri_sort(d$letter, locale = "C")
# > [1] "A" "C" "Z" "b" "e"
どうもstringi
はライブラリとしてICUを使用しているため、システムのロケールを変更した場合とは挙動が異なるみたいです。ICUの場合、C
は実際にはen_US_POSIX
を指定したのと同じことになるようです(cf. Locale - ICU User Guide)。
すなわちICUでできることはできる、ということなので、こんな感じで細かいソートの制御をすることもできます。これはこれで使いみちがありそうです。
## locale="en"のデフォルトのソートは小文字が前に出る
str <- c("a", "A", "a", "A")
stringi::stri_sort(str, locale = "en")
# > [1] "a" "a" "A" "A"
## colCaseFirstの指定で大文字を前にできる
stringi::stri_sort(str, locale = "en@colCaseFirst=upper")
# > [1] "A" "A" "a" "a"
## 通常のソートは辞書順
str2 <- c("A1", "A12", "A112")
stringi::stri_sort(str2, locale = "en")
# > [1] "A1" "A112" "A12"
## 数字の大きさを考慮したソート(自然順ソート)ができる
stringi::stri_sort(str2, locale = "en@colNumeric=yes")
# > [1] "A1" "A12" "A112"