ページ

2012/09/02

[競技プログラミング][PHP][天下一プログラマーコンテスト2012]camel_case

B - camel_case

時間制限 : 2sec / スタック制限 : 10MB / メモリ制限 : 64MB

問題文

以下の通りに、「キャメルケースの文字列」と「アンダースコア区切りの文字列」を定めます。

単語
英小文字で始まる、英小文字・数字から成る文字列
キャメルケースの文字列
1つ以上の単語を、最初の単語以外の先頭の文字を大文字にして連結した文字列
アンダースコア区切りの文字列
1つ以上の単語を、単語の間を1つのアンダースコアで区切って連結した文字列
また、キャメルケースの文字列とアンダースコア区切りの文字列には、
連結した後、最初や最後に1つ以上のアンダースコアが追加されることがあります。

文字列が与えられるので、キャメルケースの文字列とアンダースコア区切りの文字列を相互変換してください。
追加された行頭・行末のアンダースコアはそのまま保持するものとし、
キャメルケースの文字列でもアンダースコア区切りの文字列でもない場合は
与えられた文字列をそのまま出力するものとします。
また、キャメルケースの文字列とアンダースコア区切りの文字列の両方に解釈できる文字列は
どちらであると考えても相互変換した結果が等しくなることが保証されます。

入力

入力は以下の形式で標準入力から与えられる。
c1c2…cN
入力として文字列が 1 行で与えられる。
入力の文字列長 N は、 1≤N≤50 を満たす。
i 文字目の文字 ci は 大文字のアルファベット (A, …, Z) 、
小文字のアルファベット (a, …, z) 、 数字 (0, …, 9) 、 アンダースコア (_ ) のいずれかである。

出力

入力として与えられた文字列を相互変換した文字列を標準出力に 1 行で出力せよ。
なお、行の終端には改行が必要である。

出典

B: camel_case - 天下一プログラマーコンテスト2012 予選B | AtCoder

回答

preg_replaceのe修飾子版
Submission #41990 - 天下一プログラマーコンテスト2012 予選B | AtCoder
AtCoder/tenka1_2012_B_2_preg_replace.php at master · wada811/AtCoder


preg_replace_callback版
Submission #41989 - 天下一プログラマーコンテスト2012 予選B | AtCoder
AtCoder/tenka1_2012_B_2_preg_replace_callback.php at master · wada811/AtCoder · GitHub

AtCoder上の回答を載せているのは通らなかったから…。
<?php
$variable = trim(fgets(STDIN));
if(isCamelCase($variable)){
    $variable = toSnakeCase($variable);
}else if(isSnakeCase($variable)){
    $variable = toCamelCase($variable);
}
echo $variable.PHP_EOL;
 
function isCamelCase($str){
    return preg_match('/^_*[a-z][a-z0-9]+([A-Z][a-z0-9]+)+_*$/', $str, $matches);
}
function toCamelCase($str){
    return preg_replace('/([a-z0-9])_([a-z])/e', '$1.strtoupper("$2")', $str);
}
function isSnakeCase($str){
    return preg_match('/^_*[a-z][a-z0-9]+(_[a-z][a-z0-9]+)+_*$/', $str, $matches);
}
function toSnakeCase($str){
    return preg_replace('/([a-z0-9])([A-Z])/e', '$1."_".strtolower("$2")', $str);
}
?>
preg_replaceだけで使えるe修飾子を使えば第二引数をPHPコードとして評価してくれます。
セキュリティ的にヤバイらしいので下のpreg_replace_callbackを使えと言われています。
<?php
$variable = trim(fgets(STDIN));
if(isCamelCase($variable)){
    $variable = toSnakeCase($variable);
}else if(isSnakeCase($variable)){
    $variable = toCamelCase($variable);
}
echo $variable.PHP_EOL;
 
function isCamelCase($str){
    return preg_match('/^_*[a-z][a-z0-9]+([A-Z][a-z0-9]+)+_*$/', $str, $matches);
}
function toCamelCase($str){
    return preg_replace_callback(
        '/([a-z0-9])_([a-z])/',
        create_function('$matches', 'return $matches[1].strtoupper($matches[2]);'),
        $str);
}
function isSnakeCase($str){
    return preg_match('/^_*[a-z][a-z0-9]+(_[a-z][a-z0-9]+)+_*$/', $str, $matches);
}
function toSnakeCase($str){
    return preg_replace_callback(
        '/([a-z0-9])([A-Z])/', 
        create_function('$matches', 'return $matches[1]."_".strtolower($matches[2]);'),
        $str);
}
?>
こちらは第二引数がコールバック関数になっているのでcreate_functionで作ってやります。
中でstrtoupperしたり、strtolowerしたりしてキャメルケースとスネークケースを相互変換してます。

綺麗なソースだろ?通らないんだぜ…これ…

どうやら正規表現の判定がミスっているらしいのだが…。
連結した後、最初や最後に1つ以上のアンダースコアが追加されることがあります。
最初と最後のアンダースコアをマッチ: /^_*(hoge)_*$/
単語
英小文字で始まる、英小文字・数字から成る文字列
単語: /[a-z][a-z0-9]+/
キャメルケースの文字列
1つ以上の単語を、最初の単語以外の先頭の文字を大文字にして連結した文字列
キャメルケース: /[a-z][a-z0-9]+([A-Z][a-z0-9]+)+/
最終的なキャメルケースの正規表現: /^_*[a-z][a-z0-9]+([A-Z][a-z0-9]+)+_*$/
ということでこれはよさそう。
アンダースコア区切りの文字列
1つ以上の単語を、単語の間を1つのアンダースコアで区切って連結した文字列
スネークケース: /[a-z][a-z0-9]+(_[a-z][a-z0-9]+)+/
単語をアンダースコア区切りで繰り返すのでこれで良いはず。
最終的なスネークケースの正規表現: /^_*[a-z][a-z0-9]+(_[a-z][a-z0-9]+)+_*$/
これも良さそうなのだが何やらAtCoderの結果を何度か見ていると
スネークケースが漏れているんじゃないかと思うようなことがあった。勘だけど…。

わからないのでわかった方ブログのコメントとかTwitterとかで教えてください。
お願いします。