Rcppで全角英数を半角英数に変換するパッケージをつくった

地価のデータとGISのデータを合わせようとしたら数字が半角だったり全角だったりして、

アイエエエ! ナンデ!? 全角ナンデ!?

平成25年地価公示価格(東京都分)
位置参照情報ダウンロードサービス

ってなって、

つくりました。

yutannihilation/halfwidthr

インストール

library(devtools)
install_github("yutannihilation/halfwidthr")

使い方

こんな感じです。地味!

> halfwidthen(c("123", "東京都千代田区千代田1番1号"))
[1] "123"                        "東京都千代田区千代田1番1号"

だがしかし…

  • そういえばC++ほとんど触ったことなかった
  • 文字コードをバイト列としていじるとか狂気の沙汰だった
  • Rcppで文字列をいじってる例あんまりなかった
  • つい出来心でC++11使ってみた

などなど、反省点が多数。。C++スキルゼロの私ごときが、文字コードの闇を垣間見ながらやるにはきついやつでした。

速度とか必要ないので、分かりやすさが一番です。Rcppとか使わず、ふつうに↓のやり方にすればよかった。。

Rで全角数字を半角数字に書き換える - StatsBeginner: 初学者の統計学習ノート

まあでもせっかくなのでRcpp触って苦しんだところをメモしておきます。

std::vector<std::string> と Rcpp::CharacterVector の違い

CharacterVectorはRcppのクラスです。文字列のベクターで、std::vector<std::string>に相当するものです。 文字列のスカラにはStringというクラスが用意されています。std::stringに相当するものです。

NA の扱い

基本的にはどちらもベクターとしての性質を持ちますが、std::stringだとR固有の値を扱えません。たとえば、NAstd::stringに変換すると、"NA"という文字列になってしまいます。

> library(Rcpp)
> cppFunction("std::vector<std::string> stdvec(std::vector<std::string> str) {
+                return str;
+              }")
> cppFunction("CharacterVector chrvec(CharacterVector str) {
+                return str;
+              }")
> x <- c("a", NA)
> chrvec(x)
[1] "a" NA 
> stdvec(x)
[1] "a"  "NA"

ループの時にNAだけ飛ばしたいときはis_na()という関数が各クラスに用意されているので、

for (int i = 0; i < str.size(); ++i) {
  if( CharacterVector::is_na(str[i]) ){

    // do something...
  }
}

のようにします。NAの扱いについてはHadleyさんのこちらの記事が参考になりました。

Working with Missing Values

変換

std::stringに変換するにはRcpp::as<T>()という関数を使います。こんな感じ。

as< std::vector<std::string> >(str)

ただ、上述のように変換してしまうとNAがうまく取り扱えません。 NA以外の時だけ処理をしたい場合は、以下のようにします。

for( int i = 0; i < str.size(); i++){
  if( CharacterVector::is_na(str[i]) ) continue;

  std::string std_str = as<std::string>(str[i]);

  // do something...
}

roxygen2 との組み合わせ

cppファイルでもroxygen2でドキュメントが書けます。

↓こちらの@teramonagiさんの例が参考になりました。教えていただいてありがとうございました!

rOpenWeatherMap/example.cpp at master · teramonagi/rOpenWeatherMap · GitHub

Rファイルの時は行頭に#'と書いていましたが、cppファイルの場合は//'と書きます。

こんな感じ。

//' @useDynLib packagename

//' Do something
//'
//' @param num int
//' @description This function does something with a given int.
//' @export
// [[Rcpp::export]]
void func(int num) {

  // do something...

@export と Rcpp::export

ここで@exportだけでいいのでは?と思ってたんですが、[[Rcpp::export]]も必要らしいです。その理由はこちら。

If you’re familiar with roxygen2, you might wonder how this relates to @export. Rcpp::export controls whether a function is exported from C++ to R; @export controls whether a function is exported from a package and made available to the user. (adv-r.had.co.nz/Rcpp.html)

@exportは関数をパッケージにエクスポートするかを、[[Rcpp::export]]は関数をC++からRにエクスポートするかどうかを、それぞれコントロールしているらしいです。

@useDynLib

@useDynLibはR以外のライブラリを使うときに指定するアノテーションです。これはcppファイルのどれかに書いておけば大丈夫みたいですが、ヘッダファイルだとだめでした。そらそうか。

C++11

RcppでもC++11を使えます。

First steps in using C++11 with RcppによるとRcpp 0.10.3以降なら以下のように書くだけでいけるはずです。

// [[Rcpp::plugins(cpp11)]]

が、なぜか動きません。。sourceCpp()で個別にファイルをコンパイルした時は利いてるんですが、RStudioでビルドしたときとかdevtools::install_githubしたときは有効にならないみたいです。

たぶん私が分かっていないだけなので、詳しい方教えてください。。

回避策(?)

githubを見回った感じ、Makevarsというファイルで環境変数を設定すると大丈夫らしいです。ので、とりあえずそれを踏襲することにしました。

halfwidthr/Makevars at master · yutannihilation/halfwidthr · GitHub

もうちょっと調べたかったところ

テスト

テストはtestthatで書いたのでRにエクスポートした関数のみしかテストできません。 エクスポートしないC++の関数も、C++側でユニットテストできる気がするんですが調べるのめんどくさくて諦めました。

@useDynLib を書かない方法

dplyrのコードを検索すると、自動生成された一行しかありません。たぶん明示的に書かなくてもいい方法あるんだろうなーと思いましたが、これも調べるのめんどくさくて諦めました。

まとめ

Rcppはたしかに可能性を感じます。が、C++ド素人がいきなりやるにはつらいなーという感じでした。当たり前ながら。 まあでも便利そう。徐々に慣れていければいいなという感じでした。