01. スネークケースとキャメルケースを変換する
user_id と userId を行き来させます。データベースの列名と JSON のキー、定数と変数のように、名前の書き方をそろえるときに使います。HTTPServer のように大文字が続く名前で、よく見かける書き方はほとんどの言語で崩れます。 ほかの言語・正解の例・解説 →
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(""))
}
02. HTML の特殊文字をエスケープする
利用者が入力した文字を HTML に埋め込む前に、< > & " ' を < などに置き換えます。標準の関数は言語ごとに ' の書き方が違い、この決まりの形とそのまま一致したのは Ruby だけでした。 ほかの言語・正解の例・解説 →
escape.kt2.4.21 で 8 / 8 件の出力が正解と一致
// 受け取った文字列の & < > " ' を、HTML の書き方(& < > " ')に変える
fun main(args: Array<String>) {
val s = args[0]
// Kotlin(JVM)の標準にはエスケープの関数がないので、1文字ずつ見て置き換える。
// 5文字はどれも1つの Char に収まるので、絵文字(Char 2つ)はそのまま書き写される
val escaped = buildString {
for (c in s) {
when (c) {
'&' -> append("&")
'<' -> append("<")
'>' -> append(">")
'"' -> append(""")
'\'' -> append("'")
else -> append(c)
}
}
}
println(escaped)
}
03. 順番を保って重複を消す
同じ値の2回目以降を消し、初めて出た順に残します。タグの一覧や、選んだ項目の一覧でよく使います。集合(Set)に入れて戻す書き方では、言語によって順番が変わったり、実行するたびに変わったりします。 ほかの言語・正解の例・解説 →
unique.kt2.4.21 で 9 / 9 件の出力が正解と一致
// 受け取った文字列から、2回目以降に出たものを消し、初めて出た順に出す
fun main(args: Array<String>) {
// distinct は初めて出た順を保つ(中は LinkedHashSet)。hashSetOf に入れて出すと並びがハッシュ値の順になる
val out = args.distinct()
for (s in out) {
println(if (s.isEmpty()) "(空)" else s)
}
println("${out.size}件(${args.size - out.size}件を消した)")
}
04. 日付と時刻を ISO 8601 で読み書きする
API やログでよく使う 2026-10-30T09:00:00+09:00 の形を読み、UTC と日本時間で書き直します。標準のパーサーにそのまま渡すと、2月30日を3月2日として読んだり、タイムゾーンの無い日時を動かす場所の時間帯で読んだりします。 ほかの言語・正解の例・解説 →
iso8601.kt2.4.21 で 15 / 15 件の出力が正解と一致
// ISO 8601 の日時を受け取って、UTC と日本時間で書き出す
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
// 標準の ISO の書式は大文字小文字を区別せず(「t」「z」も通る)、「+09:00:00」も読む。形は先に正規表現で決める
val ISO = Regex("""[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?(Z|[+-][0-9]{2}:[0-9]{2})?""")
fun convert(s: String): String {
// matchEntire は文字列全体が合うときだけ当たる(find は途中に合えば当たる)
val m = ISO.matchEntire(s) ?: return "NG: 日時の形ではありません"
return try {
if (m.groupValues[2].isEmpty()) {
// タイムゾーンが無くても、2月30日なら「形が違う」を返したいので、日時としては読んでみる
LocalDateTime.parse(s)
return "NG: タイムゾーン(Z か +09:00 など)を付けてください"
}
// ISO の書式は STRICT で読むので、2月30日・25時はエラーになる
val t = OffsetDateTime.parse(s)
// ZoneId.systemDefault() を使わず、時差だけ(ZoneOffset)で変える
val utc = t.withOffsetSameInstant(ZoneOffset.UTC)
val jst = t.withOffsetSameInstant(ZoneOffset.ofHours(9))
// 年は yyyy(紀元の年)ではなく uuuu。秒の小数は書式に入れないので切り捨てになる
"UTC " + utc.format(DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss'Z'")) + "\n" +
"日本時間 " + jst.format(DateTimeFormatter.ofPattern("uuuu年MM月dd日 HH:mm:ss"))
} catch (e: DateTimeParseException) {
"NG: 日時の形ではありません"
}
}
fun main(args: Array<String>) {
println(convert(args[0]))
}
05. アクセスログを集計する
Web サーバーのアクセスログを標準入力から読み、ステータスごとの件数と、応答の遅いリクエストの上位3件を出します。空白で区切って何番目かを取る書き方は手軽ですが、壊れた行を数えたり、応答秒を文字のまま並べたりします。 ほかの言語・正解の例・解説 →
access_log.kt2.4.21 で 8 / 8 件の出力が正解と一致
// 標準入力のアクセスログを読んで、件数・ステータスごとの件数・遅い順の上位3件を出す
// 日時やHTTPの版には空白が入るので、空白で区切って何番目と数えず、行の形そのものを正規表現で確かめる
val LOG = Regex("""\S+ \S+ \S+ \[[^\]]+\] "(\S+) (\S+) [^"]+" ([0-9]{3}) ([0-9]+|-) ([0-9]+\.[0-9]+)""")
data class Row(val sec: String, val method: String, val path: String, val status: String)
fun main() {
val rows = mutableListOf<Row>()
var bad = 0
// readLine は \n でも \r\n でも行を区切り、行の終わりの記号を取って返す
while (true) {
val line = readLine() ?: break
if (line.isEmpty()) continue
// find は行のどこかに合えば通る。行全体が形どおりか確かめるのは matchEntire
val m = LOG.matchEntire(line)
if (m == null) {
bad++
continue
}
val (method, path, status, _, sec) = m.destructured
rows.add(Row(sec, method, path, status))
}
println("件数 ${rows.size}")
if (bad > 0) println("読めない行 $bad")
println("ステータス")
// groupingBy { }.eachCount() は入れた順の Map を返す。toSortedMap でキーの小さい順にする
for ((status, n) in rows.groupingBy { it.status }.eachCount().toSortedMap()) {
println("$status $n")
}
println("遅い順")
// sortedByDescending { it.sec } では文字として比べてしまい "10.5" が "9.8" より後になる。toDouble してから比べる。
// Kotlin の並べ替えは安定(同じ秒なら先に出た行が先)
for (r in rows.sortedByDescending { it.sec.toDouble() }.take(3)) {
println("${r.sec} ${r.method} ${r.path}")
}
}
06. URL 用の文字列(slug)を作る
記事のタイトルから「hello-world」のような URL の一部を作ります。ブログや商品ページのアドレスを、見て意味が分かる形にするときに使います。よく見かける書き方は、日本語のタイトルをそのまま URL に入れたり、空の slug を黙って返したりします。 ほかの言語・正解の例・解説 →
slug.kt2.4.21 で 14 / 14 件の出力が正解と一致
// 文字列を URL に使える形(slug)に変える
const val MAX = 32
fun main(args: Array<String>) {
// lowercase() は Unicode の決まりで変えるので、İ が2文字になる。A〜Z だけを変える
var s = args[0].replace(Regex("[A-Z]")) { it.value.lowercase() }
// \w は既定では ASCII の [A-Za-z0-9_]。残す文字を [a-z0-9] と書く
s = s.replace(Regex("[^a-z0-9]+"), "-").trim('-')
if (s.length > MAX) {
s = s.substring(0, MAX).trimEnd('-')
}
println(if (s.isEmpty()) "NG: URL に使える文字がありません" else s)
}
07. URL エンコードする
検索語や日本語の名前を、URL のクエリの値(`?q=...`)に入れられる形に変えます。よく見かける書き方は、空白を「+」にしたり、`!` や `~` を変えずに残したりして、受け取る側で別の文字列になります。 ほかの言語・正解の例・解説 →
url_encode.kt2.4.21 で 15 / 15 件の出力が正解と一致
// 文字列を URL のクエリの値に入れられる形(パーセントエンコード)に変える
fun main(args: Array<String>) {
// java.net.URLEncoder は空白を「+」、「~」を %7E にして「*」を残す(フォームの形)
val sb = StringBuilder()
for (b in args[0].toByteArray(Charsets.UTF_8)) {
val v = b.toInt() and 0xFF // Byte は符号付きなので、そのまま使うと負になる
val c = v.toChar()
if (c in 'A'..'Z' || c in 'a'..'z' || c in '0'..'9' || c in "-_.~") {
sb.append(c)
} else {
sb.append("%%%02X".format(v))
}
}
println(sb)
}