画面にこう出れば正解
どの言語で書いても、同じ入力なら画面に出る文字は1文字も違わない、というのが正解の決め方です(例は Python で動かしたときのコマンドです)。
基本(キャメル → スネーク)
$ python3 case_convert.py snake userId
user_id大文字が続く(HTTPServer)
$ python3 case_convert.py snake HTTPServer
http_server途中で大文字が続く
$ python3 case_convert.py snake getHTTPResponseCode
get_http_response_code最後が大文字の続き(userID)
$ python3 case_convert.py snake userID
user_id数字のあと
$ python3 case_convert.py snake version2Name
version2_name先頭が大文字(パスカルケース)
$ python3 case_convert.py snake UserProfile
user_profileもうスネークケース
$ python3 case_convert.py snake already_snake
already_snake基本(スネーク → キャメル)
$ python3 case_convert.py camel user_id
userId大文字のスネークケース(定数)
$ python3 case_convert.py camel MAX_RETRY_COUNT
maxRetryCount先頭と途中の「_」が続く
$ python3 case_convert.py camel __private__value
privateValue数字の部分
$ python3 case_convert.py camel address_2_line
address2Line空の文字列
$ python3 case_convert.py camel ""
日本語が入っている
$ python3 case_convert.py snake userなまえ
NG: 半角の英数字と _ だけにしてくださいハイフン(ケバブケース)は扱わない
$ python3 case_convert.py camel user-id
NG: 半角の英数字と _ だけにしてください10言語のプログラム
どれもそのまま保存して動かせます。毎朝、上の正解の例をすべて実際に動かして確かめています。
case_convert.php8.4.26 で 14 / 14 件の出力が正解と一致
<?php
// スネークケース(user_id)とキャメルケース(userId)を、指定した向きに変換する
const NG = "NG: 半角の英数字と _ だけにしてください";
[, $mode, $s] = $argv;
// preg の $ は末尾の改行の手前にも合う。D を付けて、文字列の本当の終わりだけに合わせる
if (!preg_match('/\A[A-Za-z0-9_]*\z/D', $s)) {
echo NG . "\n";
} elseif ($mode === "snake") {
// 切れ目は2種類。小文字か数字の次の大文字(userId)と、大文字の続きの最後で次が小文字のもの(HTTP|Server)
$s = preg_replace('/(?<=[a-z0-9])(?=[A-Z])/', '_', $s);
$s = preg_replace('/(?<=[A-Z])(?=[A-Z][a-z])/', '_', $s);
// strtolower は PHP 8.2 から ASCII だけを変える(ロケールに左右されない)。ここは ASCII だけなので mbstring は要らない
echo strtolower($s) . "\n";
} else {
// explode は「__」のところで空の部分を作る。array_filter の既定は "0" も捨ててしまうので、空文字だけを捨てる
$parts = array_values(array_filter(explode("_", $s), fn($p) => $p !== ""));
$out = "";
foreach ($parts as $i => $p) {
// ucfirst は先頭しか変えない。残りを先に小文字にする
$out .= $i === 0 ? strtolower($p) : ucfirst(strtolower($p));
}
echo $out . "\n";
}
動かし方(ファイルを保存したフォルダで)
$ php case_convert.php snake userId
case_convert.py3.14.7 で 14 / 14 件の出力が正解と一致
# スネークケース(user_id)とキャメルケース(userId)を、指定した向きに変換する
import re
import sys
NG = "NG: 半角の英数字と _ だけにしてください"
mode, s = sys.argv[1], sys.argv[2]
# \w は「なまえ」のようなひらがなにも合う(Python の文字列は Unicode が基本)。使ってよい文字を [A-Za-z0-9_] と書く
if not re.fullmatch(r"[A-Za-z0-9_]*", s):
print(NG)
elif mode == "snake":
# 切れ目は2種類。小文字か数字の次の大文字(userId)と、大文字の続きの最後で次が小文字のもの(HTTP|Server)
# 先読み・後読みで「位置」だけに合わせると、文字を食わないので2つの切れ目が重なっても取りこぼさない
s = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", s)
s = re.sub(r"(?<=[A-Z])(?=[A-Z][a-z])", "_", s)
print(s.lower())
else:
# split("_") は「__」のところで空の部分を作る。空の部分は捨てる
parts = [p for p in s.split("_") if p]
# capitalize() は先頭を大文字・残りを小文字にする(title() は数字のあとも大文字にするので使わない)
print("".join([parts[0].lower()] + [p.capitalize() for p in parts[1:]]) if parts else "")
動かし方(ファイルを保存したフォルダで)
$ python3 case_convert.py snake userId
case_convert.go1.27.2 で 14 / 14 件の出力が正解と一致
// スネークケース(user_id)とキャメルケース(userId)を、指定した向きに変換する
package main
import (
"fmt"
"os"
"strings"
)
func isUpper(c byte) bool { return c >= 'A' && c <= 'Z' }
func isLower(c byte) bool { return c >= 'a' && c <= 'z' }
func isDigit(c byte) bool { return c >= '0' && c <= '9' }
func main() {
mode, s := os.Args[1], os.Args[2]
// 先に半角の英数字と _ だけかを確かめる。そのあとは1バイト=1文字として扱える
for i := 0; i < len(s); i++ {
c := s[i]
if !(isUpper(c) || isLower(c) || isDigit(c) || c == '_') {
fmt.Println("NG: 半角の英数字と _ だけにしてください")
return
}
}
if mode == "snake" {
// Go の regexp(RE2)には先読み・後読みが無いので、1文字ずつ前後を見て切れ目を決める
var b strings.Builder
for i := 0; i < len(s); i++ {
c := s[i]
if i > 0 && isUpper(c) {
prev := s[i-1]
// 小文字か数字の次の大文字(userId)と、大文字の続きの最後で次が小文字のもの(HTTP|Server)
if isLower(prev) || isDigit(prev) || (isUpper(prev) && i+1 < len(s) && isLower(s[i+1])) {
b.WriteByte('_')
}
}
b.WriteByte(c)
}
fmt.Println(strings.ToLower(b.String()))
return
}
// strings.Split は「__」のところで空の部分を作る。空の部分は飛ばす
// (strings.Title は単語の区切りの決まりが違い、Go 1.18 から非推奨なので使わない)
var b strings.Builder
for _, p := range strings.Split(s, "_") {
if p == "" {
continue
}
if b.Len() == 0 {
b.WriteString(strings.ToLower(p))
} else {
b.WriteString(strings.ToUpper(p[:1]) + strings.ToLower(p[1:]))
}
}
fmt.Println(b.String())
}
動かし方(ファイルを保存したフォルダで)
$ go run case_convert.go snake userId
case_convert.js24.15.0 で 14 / 14 件の出力が正解と一致
// スネークケース(user_id)とキャメルケース(userId)を、指定した向きに変換する
const NG = "NG: 半角の英数字と _ だけにしてください";
const [mode, s] = process.argv.slice(2);
// JavaScript の \w は ASCII だけだが、言語によってはひらがなにも合う。どの言語でも同じ意味になる [A-Za-z0-9_] と書く
if (!/^[A-Za-z0-9_]*$/.test(s)) {
console.log(NG);
} else if (mode === "snake") {
// 切れ目は2種類。小文字か数字の次の大文字(userId)と、大文字の続きの最後で次が小文字のもの(HTTP|Server)
// /([A-Z])/g を "_$1" にするだけだと HTTPServer が _h_t_t_p_server になる
const out = s
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
.replace(/([A-Z])(?=[A-Z][a-z])/g, "$1_");
console.log(out.toLowerCase());
} else {
// split("_") は「__」のところで空の部分を作る。filter で捨てる
const parts = s.split("_").filter((p) => p !== "");
console.log(
parts
.map((p, i) => (i === 0 ? p.toLowerCase() : p[0].toUpperCase() + p.slice(1).toLowerCase()))
.join("")
);
}
動かし方(ファイルを保存したフォルダで)
$ node case_convert.js snake userId
CaseConvert.java21.0.12 で 14 / 14 件の出力が正解と一致
// スネークケース(user_id)とキャメルケース(userId)を、指定した向きに変換する
import java.util.Locale;
public class CaseConvert {
public static void main(String[] args) {
String mode = args[0], s = args[1];
// matches は文字列全体が合うかを見る(find は一部が合えば通ってしまう)
if (!s.matches("[A-Za-z0-9_]*")) {
System.out.println("NG: 半角の英数字と _ だけにしてください");
return;
}
if (mode.equals("snake")) {
// 切れ目は2種類。小文字か数字の次の大文字(userId)と、大文字の続きの最後で次が小文字のもの(HTTP|Server)
String out = s.replaceAll("(?<=[a-z0-9])(?=[A-Z])", "_")
.replaceAll("(?<=[A-Z])(?=[A-Z][a-z])", "_");
// toLowerCase() は動かす場所の言語の設定を使う。トルコ語だと I が「ı」になるので Locale.ROOT を渡す
System.out.println(out.toLowerCase(Locale.ROOT));
return;
}
StringBuilder sb = new StringBuilder();
// split("_") は「__」や先頭の「_」で空の部分を作る。空の部分に substring(0, 1) をすると落ちるので飛ばす
for (String p : s.split("_")) {
if (p.isEmpty()) continue;
if (sb.length() == 0) {
sb.append(p.toLowerCase(Locale.ROOT));
} else {
sb.append(p.substring(0, 1).toUpperCase(Locale.ROOT)).append(p.substring(1).toLowerCase(Locale.ROOT));
}
}
System.out.println(sb);
}
}
動かし方(ファイルを保存したフォルダで)
$ java CaseConvert.java snake userId
case_convert.cs10.0.402 で 14 / 14 件の出力が正解と一致
// スネークケース(user_id)とキャメルケース(userId)を、指定した向きに変換する
using System.Text;
using System.Text.RegularExpressions;
string mode = args[0], s = args[1];
// C# の \w は「なまえ」のようなひらがなにも合うので、[A-Za-z0-9_] と書く。
// $ は末尾の改行の手前にも合うので、文字列の本当の終わりの \z を使う
if (!Regex.IsMatch(s, @"\A[A-Za-z0-9_]*\z"))
{
Console.WriteLine("NG: 半角の英数字と _ だけにしてください");
}
else if (mode == "snake")
{
// 切れ目は2種類。小文字か数字の次の大文字(userId)と、大文字の続きの最後で次が小文字のもの(HTTP|Server)
var t = Regex.Replace(s, "(?<=[a-z0-9])(?=[A-Z])", "_");
t = Regex.Replace(t, "(?<=[A-Z])(?=[A-Z][a-z])", "_");
// ToLower() は動かす場所の言語の設定を使う(トルコ語では I が「ı」)。ToLowerInvariant を使う
Console.WriteLine(t.ToLowerInvariant());
}
else
{
// RemoveEmptyEntries で「__」のところの空の部分を捨てる。
// (TextInfo.ToTitleCase は全部大文字の単語を略語とみなして MAX のまま残すので使わない)
var sb = new StringBuilder();
foreach (var p in s.Split('_', StringSplitOptions.RemoveEmptyEntries))
{
sb.Append(sb.Length == 0
? p.ToLowerInvariant()
: char.ToUpperInvariant(p[0]) + p.Substring(1).ToLowerInvariant());
}
Console.WriteLine(sb.ToString());
}
動かし方(ファイルを保存したフォルダで)
$ dotnet run case_convert.cs -- snake userId
case_convert.rs1.99.0 で 14 / 14 件の出力が正解と一致
// スネークケース(user_id)とキャメルケース(userId)を、指定した向きに変換する
fn main() {
let args: Vec<String> = std::env::args().collect();
let (mode, s) = (args[1].as_str(), args[2].as_str());
// is_alphanumeric は「な」のような文字にも true を返す。ASCII だけを見る is_ascii_alphanumeric を使う
if !s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
println!("NG: 半角の英数字と _ だけにしてください");
return;
}
if mode == "snake" {
// 標準には正規表現が無いので、1文字ずつ前後を見て切れ目を決める(ASCII だけなのでバイトで見てよい)
let b = s.as_bytes();
let mut out = String::new();
for i in 0..b.len() {
let c = b[i];
if i > 0 && c.is_ascii_uppercase() {
let prev = b[i - 1];
// 小文字か数字の次の大文字(userId)と、大文字の続きの最後で次が小文字のもの(HTTP|Server)
let next_lower = i + 1 < b.len() && b[i + 1].is_ascii_lowercase();
if prev.is_ascii_lowercase() || prev.is_ascii_digit() || (prev.is_ascii_uppercase() && next_lower) {
out.push('_');
}
}
out.push(c.to_ascii_lowercase() as char);
}
println!("{}", out);
return;
}
// split('_') は「__」のところで空の部分を作る。filter で捨てる
let mut out = String::new();
for (i, p) in s.split('_').filter(|p| !p.is_empty()).enumerate() {
let lower = p.to_ascii_lowercase();
if i == 0 {
out.push_str(&lower);
} else {
// 文字列の添字はバイトの位置。ASCII だけと確かめてあるので [..1] で先頭1文字を切り出せる
out.push_str(&lower[..1].to_ascii_uppercase());
out.push_str(&lower[1..]);
}
}
println!("{}", out);
}
動かし方(ファイルを保存したフォルダで)
$ rustc case_convert.rs && ./case_convert snake userId
case_convert.rb4.0.7 で 14 / 14 件の出力が正解と一致
# スネークケース(user_id)とキャメルケース(userId)を、指定した向きに変換する
NG = "NG: 半角の英数字と _ だけにしてください"
mode, s = ARGV
# Ruby の ^ $ は「行」の頭と終わり。文字列全体を見るときは \A と \z を使う(改行のあとを素通りさせない)
if s !~ /\A[A-Za-z0-9_]*\z/
puts NG
elsif mode == "snake"
# 切れ目は2種類。小文字か数字の次の大文字(userId)と、大文字の続きの最後で次が小文字のもの(HTTP|Server)
puts s.gsub(/(?<=[a-z0-9])(?=[A-Z])/, "_").gsub(/(?<=[A-Z])(?=[A-Z][a-z])/, "_").downcase
else
# split("_") は「__」のところで空の部分を作る(先頭の空も残る)。reject で捨てる
parts = s.split("_").reject(&:empty?)
# capitalize は先頭を大文字・残りを小文字にする。空の配列なら join は ""
puts parts.each_with_index.map { |p, i| i.zero? ? p.downcase : p.capitalize }.join
end
動かし方(ファイルを保存したフォルダで)
$ ruby case_convert.rb snake userId
case_convert.kt2.4.21 で 14 / 14 件の出力が正解と一致
// スネークケース(user_id)とキャメルケース(userId)を、指定した向きに変換する
fun main(args: Array<String>) {
val (mode, s) = args
// matches は文字列全体が合うかを見る
if (!s.matches(Regex("[A-Za-z0-9_]*"))) {
println("NG: 半角の英数字と _ だけにしてください")
return
}
if (mode == "snake") {
// 切れ目は2種類。小文字か数字の次の大文字(userId)と、大文字の続きの最後で次が小文字のもの(HTTP|Server)
val out = s.replace(Regex("(?<=[a-z0-9])(?=[A-Z])"), "_")
.replace(Regex("(?<=[A-Z])(?=[A-Z][a-z])"), "_")
// lowercase() は言語の設定によらない(昔の toLowerCase() は設定を使った)
println(out.lowercase())
return
}
// split("_") は「__」のところで空の部分を作る。filter で捨てる
val parts = s.split("_").filter { it.isNotEmpty() }
// replaceFirstChar は先頭しか変えない。残りを先に小文字にする
println(parts.mapIndexed { i, p -> if (i == 0) p.lowercase() else p.lowercase().replaceFirstChar { it.uppercase() } }
.joinToString(""))
}
動かし方(ファイルを保存したフォルダで)
$ kotlinc case_convert.kt -include-runtime -d case_convert.jar && java -jar case_convert.jar snake userId
case_convert.swift6.3.3 で 14 / 14 件の出力が正解と一致
// スネークケース(user_id)とキャメルケース(userId)を、指定した向きに変換する
let mode = CommandLine.arguments[1]
let s = CommandLine.arguments[2]
// Character.isLetter は「な」にも true を返す。isASCII と合わせて、半角の英数字だけを通す
func isUpper(_ c: Character) -> Bool { c.isASCII && c.isUppercase }
func isLower(_ c: Character) -> Bool { c.isASCII && c.isLowercase }
func isDigit(_ c: Character) -> Bool { c.isASCII && c.isNumber }
if !s.allSatisfy({ isUpper($0) || isLower($0) || isDigit($0) || $0 == "_" }) {
print("NG: 半角の英数字と _ だけにしてください")
} else if mode == "snake" {
// Swift の文字列は整数の添字で引けないので、配列にしてから前後の文字を見る
let cs = Array(s)
var out = ""
for (i, c) in cs.enumerated() {
if i > 0 && isUpper(c) {
let prev = cs[i - 1]
// 小文字か数字の次の大文字(userId)と、大文字の続きの最後で次が小文字のもの(HTTP|Server)
let nextLower = i + 1 < cs.count && isLower(cs[i + 1])
if isLower(prev) || isDigit(prev) || (isUpper(prev) && nextLower) {
out += "_"
}
}
out += c.lowercased()
}
print(out)
} else {
// split は既定で空の部分を捨てる(omittingEmptySubsequences: true)。「__」でも空の部分はできない
let parts = s.split(separator: "_")
var out = ""
for (i, p) in parts.enumerated() {
// Foundation の capitalized は単語の区切りを自分で決めるので、先頭1文字と残りを自分で分ける
out += i == 0 ? p.lowercased() : p.prefix(1).uppercased() + p.dropFirst().lowercased()
}
print(out)
}
動かし方(ファイルを保存したフォルダで)
$ swift case_convert.swift snake userId
よく見かける書き方で試すと
仕事でよく見かける書き方(標準の関数をそのまま使う形や、よく紹介される正規表現など)を、同じ正解表で動かした結果です。言語を出した答えごとにまとめ、赤い字はその言語が実際に出した間違った行です。
| 入力 | よく見かける書き方の答え(10言語) |
|---|---|
基本(キャメル → スネーク)snake userId | ✓10言語すべて正解 |
大文字が続く(HTTPServer)snake HTTPServer |
|
途中で大文字が続くsnake getHTTPResponseCode |
|
最後が大文字の続き(userID)snake userID |
|
数字のあとsnake version2Name |
|
先頭が大文字(パスカルケース)snake UserProfile |
|
もうスネークケースsnake already_snake | ✓10言語すべて正解 |
基本(スネーク → キャメル)camel user_id | ✓10言語すべて正解 |
大文字のスネークケース(定数)camel MAX_RETRY_COUNT |
|
先頭と途中の「_」が続くcamel __private__value |
|
数字の部分camel address_2_line |
|
空の文字列camel "" | ✓10言語すべて正解 |
日本語が入っているsnake userなまえ |
|
ハイフン(ケバブケース)は扱わないcamel user-id |
|
この書き方の決まり
- 引数は「変換の向き(
snakeかcamel)」と「変える文字列」 - snake(userId → user_id): 小文字か数字のあとの大文字(user|Id)と、大文字が続いたあとで次が小文字の大文字(HTTP|Server)を、単語の切れ目にする。切れ目に「_」を入れて全部を小文字にする
- camel(user_id → userId): 「_」で区切って空の部分は捨て、最初の部分は全部小文字、2つ目からは先頭だけ大文字にしてつなぐ(MAX_RETRY_COUNT → maxRetryCount)
- 扱うのは半角の英数字と「_」だけ。それ以外が入っていたら NG
解説
大文字が続く名前(HTTPServer)
よく紹介される「大文字の前に _ を入れる」書き方は、HTTPServer を h_t_t_p_server にばらします。逆に「小文字のあとの大文字だけ」で切ると httpserver とくっつきます。この表の10言語のよく見かける書き方のうち、getHTTPResponseCode を正しく変えられたのは Ruby(Rails の underscore と同じ形の2本の置き換え)だけでした。
正しくは、切れ目を2種類に分けて調べます。
(?<=[a-z0-9])(?=[A-Z])… 小文字か数字のあとの大文字の前(?<=[A-Z])(?=[A-Z][a-z])… 大文字が続いたあと、次が小文字になる大文字の前
Go の正規表現(RE2)には (?<=…)(後読み)と (?=…)(先読み)がありません。Rust には標準の正規表現がありません。この2言語と Swift は、1文字ずつ前後の文字を見て切れ目を決めています。
「先頭だけ大文字」の関数は、残りを小文字にしない
camel 側で MAX_RETRY_COUNT を変えると、関数によって答えが分かれました。
| 関数 | RETRY が | 結果 |
|---|---|---|
Python・Ruby の capitalize | Retry(残りを小文字に) | 正しい形にできる |
PHP の ucfirst・ucwords、Kotlin の replaceFirstChar | RETRY(残りはそのまま) | mAXRETRYCOUNT など |
C# の TextInfo.ToTitleCase | RETRY(全部大文字の単語は略語とみなして残す) | mAXRETRYCOUNT |
小文字にする関数と、動かす場所の言語の設定
Java の toLowerCase() と C# の ToLower() は、動かす場所の言語の設定を使います。トルコ語の設定では I が点のない ı になり、ID が ıd になります。この書き方では Java は toLowerCase(Locale.ROOT)、C# は ToLowerInvariant() を使っています。
「英数字だけか」の確かめ方
\w や「英数字か」を調べる関数が、ひらがなや漢字にも合う言語があります(Python・C# の \w、Rust の is_alphanumeric、Swift の CharacterSet.alphanumerics)。userなまえ を通してしまうので、使ってよい文字を [A-Za-z0-9_] と書いて決めています(入門 第16回 と同じ問題)。
区切ったときの空の部分
__private__value を「_」で区切ると、空の部分ができます。Java の naive は空の部分の先頭を取り出そうとして、例外で止まりました(StringIndexOutOfBoundsException)。空の部分を捨ててから使います。