備忘ログ

チラシの裏的備忘録&メモ

Rでアラビア数字から漢数字に変換する方法を考えてみたその2

追記:いろいろ関数の機能追加したものをパケージ化してGithubに上げた

indenkun.hatenablog.com

以前つくった、アラビア数字(算用数字)を漢数字変換する関数を少し改良した。

indenkun.hatenablog.com

具体的には、逐字的にアラビア数字を漢数字に置き換えるarabic2kansujiは変わりないが、アラビア数字を漢数字に計算して(1234なら千二百三十四)に変換してやるarabic2kansuji_numについて以前のバージョンでは9999までしか対応していなかったが、1万を超えて対応できるようにした。おまけでマイナスの値にも対応した。

ただし、17桁以上の数字についてはR上で正しく扱えない可能性があるので警告が出るようにした。

また、後述する問題により、特定の数字(10n(n >= 5)系)は2147483647より小さくないと受け付けられない。

改良版自作関数

依存関係は{stringer}パッケージと{purrr}パッケージだけ……だと思う。

最初のarabic2kansujiは前のと変わりなし。

arabic2kansuji_numはコメントを入れて自分がわかりやすいようにした。

arabic2kansuji <- function(num, zero = c("〇", "零")){
  
  zero <- match.arg(zero)
  
  arabicn <- "1234567890"
  if(zero == "〇") kansuji <- "一二三四五六七八九〇"
  else if(zero == "零") kansuji <- "一二三四五六七八九零"
  arabicn <- unlist(stringr::str_split(arabicn, ""))
  kansuji <- unlist(stringr::str_split(kansuji, ""))
  names(kansuji) <- arabicn
  
  stringr::str_replace_all(num, kansuji)
  
}

arabic2kansuji_num <- function(num, ...){
  if(!is.numeric(num)) stop("only number can convert to kansuji.")
  # 負の値の場合を受けるけるか受け付けないか……
  # 負の値を受け付ける場合にはこのまま次の行をコメントアウトしておく
  # if(num < 0) stop("only positive number can convert to kansuji.")
  # 負の値を受け付けない場合には次のif文をコメントアウトする
  negative <- 0
  if(num < 0){
    # 負の値だということのマーカーを付けた上で数字を絶対値化する
    negative <- 1
    num <- abs(num)
  }
  # 与えられた数字を一文字ごとに区切ってベクトルにする
  n <- purrr::reduce(stringr::str_split(num,
                          stringr::boundary(type = "character")), c)
  n <- suppressWarnings(as.numeric(n))
  
  if(anyNA(n)){
    if(num > 2147483647) stop("too large number and not good shape to convert.")
    num <- as.integer(num)
    n <- purrr::reduce(stringr::str_split(num,
                                          stringr::boundary(type = "character")), c)
    n <- as.numeric(n)
  }
  
  # 17桁以上はR側の問題でうまく処理できないので警告を出す
  if(length(n) >= 17) warning("too long to convert.")
  
  # nに一の位なら1、十の位なら2と桁数の名前がつくようにする
  m <- max(which(n >= 0)) - which(n >= 0) + 1
  names(n) <- m
  
  # 十、百、千の桁に対して桁数を4で割ったときのあまりで条件づけし
  # それぞれ文字列を付与
  for(i in 1:length(n)){
    if(as.numeric(names(n[i])) %% 4 == 1){
      if(n[i] >= 1) n[i] <- arabic2kansuji(n[i])
    }
    else if(as.numeric(names(n[i])) %% 4 == 2){
      if(n[i] >= 2) n[i] <- paste0(arabic2kansuji(n[i]), "十")
      else if(n[i] == 1) n[i] <- "十"
    }
    else if(as.numeric(names(n[i])) %% 4 == 3){
      if(n[i] >= 2) n[i] <- paste0(arabic2kansuji(n[i]), "百")
      else if(n[i] == 1) n[i] <- "百"
    }
    else if(as.numeric(names(n[i])) %% 4 == 0){
      if(n[i] >= 2) n[i] <- paste0(arabic2kansuji(n[i]), "千")
      else if(n[i] == 1) n[i] <- "千"
    }
  }
  
  # 万、億、兆、京の桁数を付与
  # 各桁の箇所で数字が存在しかつ、0ではない場合に桁の下の方から見ていって
  # 一番最初に数字を見つけたところにそれぞれの文字を付与し、付与できたらbreakする
  # 万
  if(length(n) >= 5){
    if(length(n) >= 9) l <- length(n) - 7
    else l <- 1
    for(i in (length(n) - 4):l){
      if(n[i] != 0){
        n[i] <- paste0(n[i], "万")
        break
      }
    }
  }
  # 億
  if(length(n) >= 9){
    if(length(n) >= 13) l <- length(n) - 11
    else l <- 1
    for(i in (length(n) - 8):l){
      if(n[i] != 0){
        n[i] <- paste0(n[i], "億")
        break
      } 
    }
  }
  # 兆
  if(length(n) >= 13){
    if(length(n) >= 17) l <- length(n) - 15
    else l <- 1
    for(i in (length(n) - 12):l){
      if(n[i] != 0){
        n[i] <- paste0(n[i], "兆")
        break
      } 
    }
  }
  # 京
  if(length(n) >= 17){
    if(length(n) >= 21) l <- length(n) - 19
    else l <- 1
    for(i in (length(n) - 16):l){
      if(n[i] != 0){
        n[i] <- paste0(n[i], "京")
        break
      } 
    }
  }
  
  # 0は漢数字にしたときにいらないのでNAにあとで消すため変換しておく
  n <- gsub("0", NA, n)
  # 負の値を受け付けない場合にはこの行をコメントアウトする
  # 負の値のマーカーを見て漢数字の頭に"負"とつける
  if(negative == 1) n <- c("負", n)
  # NAを消して、漢数字がはいってるベクトルをくっつける
  stringr::str_flatten(na.omit(n))
  
}

使ってみる

arabic2kansuji

arabic2kansujiは以前と変わらない。逐字的にアラビア数字を漢数字に置き換えているだけ。

arabic2kansuji(1234567890)
## [1] "一二三四五六七八九〇"
arabic2kansuji("1234567890")
## [1] "一二三四五六七八九〇"

数字で与えても内部で文字列として処理されるので数字だけならダブルコーテーションマークで囲んでも囲まなくても変わりはない。

アラビア数字以外の文字列も受け付ける。アラビア数字だけならコーテーションマークで囲う必要がないが、文字列だと必要。

arabic2kansuji("昭和64年は1989年1月7日までです。")
## [1] "昭和六四年は一九八九年一月七日までです。"
arabic2kansuji("東京都新宿区西新宿2丁目8−1")
## [1] "東京都新宿区西新宿二丁目八-一"

0についてデフォルトで〇に変換されるが、引数のzeroで零を指定すると変換できる。

arabic2kansuji("30にして立つ")
## [1] "三〇にして立つ"
arabic2kansuji("30にして立つ", zero = "零")
## [1] "三零にして立つ"

半角数字しか変換しないので全角数字は変換せずそのまま保持する。

arabic2kansuji("東京都新宿区西新宿2丁目8−1")
## [1] "東京都新宿区西新宿2丁目8-1"

arabic2kansuji_num

arabic2kansujiは9999を超えるアラビア数字も対応できるようにした。

これで2019年の日本の人口の124271318も変換できる。

arabic2kansuji_num(124271318)
## [1] "一億二千四百二十七万千三百十八"

相変わらず半角数字しか受け付けない。

arabic2kansuji_num("124271318人")
## Error in arabic2kansuji_num("124271318人"): only number can convert to kansuji.

ただし、負の値も受け付けるようにした。

arabic2kansuji_num(-123456789)
## [1] "負一億二千三百四十五万六千七百八十九"

ただ、負の値の漢数字の記法がこれでいいのかは疑問があるが。

arabic2kansuji_num(1234567890123456789)
## Warning in arabic2kansuji_num(1234567890123456768): too long to convert.

## [1] "百二十三京四千五百六十七兆八千九百一億二千三百四十五万六千七百六十八"

17桁以上だとR側の処理の都合で細かいところで正確に処理できないので一応変換はしてみるが警告を出す。

上に書いた、特定の値の問題については例えば100000が1e+5となるのだけれど、このまま中で保持されるためうまく処理できないので、as.integerで整数にもどしてやるんだけど、これができるのが32bit範囲の2147483647までなので、これを超えると変換できなくなる。だいたい10n(n >= 5)で問題になる。

arabic2kansuji_num(10000000000)
## Error in arabic2kansuji_num(1e+10): too large number and not good shape to convert.

あと、ループ処理が多いので遅い。

今後の課題

  • arabic2kansuji_numで1万以上も対応できるようにしたい。 今回達成した。

  • arabic2kansuji_numで漢数字以外の文字列を保持したまま変換できるようにしたい。

    • これについては、文字列から数字だけを抽出して取り出したときに、取り出した場所が分かればもとも場所の一番最初のところに漢数字の文字列をもどして、残りの数字消えたところNAにしてna.omitしてくっつければ行けるんじゃないかと思ってるが、ちょっと大変そう。
  • ループ処理じゃない方法でスマートにやりたい。

    • ループの箇所で京~万の桁を処理しているところがあるので難しそうなのでなしにする。
  • パッケージ化してgithubとかにあげておきたい、気持ちが上向いたら\<-NEW

    • 引数のところで非ASCII文字があるのでちょっと考える。

追記:冒頭に書いたようにパッケージ化してGithubに上げた