CODE Polyglot / よく使う書き方 / Kotlin

Kotlin のよく使う書き方(7本)

開発でよく書くコードの型と小さなツールの、Kotlin の版をまとめました。標準の機能だけで書いていて、毎朝、実際に動かして答えを確かめています。

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 に埋め込む前に、< > & " ' を &lt; などに置き換えます。標準の関数は言語ごとに ' の書き方が違い、この決まりの形とそのまま一致したのは Ruby だけでした。 ほかの言語・正解の例・解説 →

escape.kt2.4.21 で 8 / 8 件の出力が正解と一致

// 受け取った文字列の & < > " ' を、HTML の書き方(&amp; &lt; &gt; &quot; &#39;)に変える
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("&amp;")
                '<' -> append("&lt;")
                '>' -> append("&gt;")
                '"' -> append("&quot;")
                '\'' -> append("&#39;")
                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)
}

ほかの言語