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

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

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

01. スネークケースとキャメルケースを変換する

user_id と userId を行き来させます。データベースの列名と JSON のキー、定数と変数のように、名前の書き方をそろえるときに使います。HTTPServer のように大文字が続く名前で、よく見かける書き方はほとんどの言語で崩れます。 ほかの言語・正解の例・解説 →

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)
}

02. HTML の特殊文字をエスケープする

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

escape.swift6.3.3 で 8 / 8 件の出力が正解と一致

// 受け取った文字列の & < > " ' を、HTML の書き方(&amp; &lt; &gt; &quot; &#39;)に変える
// Swift の標準ライブラリにはエスケープの関数がないので、1文字ずつ見て置き換える
let s = CommandLine.arguments[1]

var out = ""
// Swift の Character は「見た目の1文字」で、< のあとに結合文字が続くと2つで1文字になり '<' と一致しない。
// unicodeScalars で、コードポイント1つずつ見る
for u in s.unicodeScalars {
    switch u {
    case "&": out += "&amp;"
    case "<": out += "&lt;"
    case ">": out += "&gt;"
    case "\"": out += "&quot;"
    case "'": out += "&#39;"
    default: out.unicodeScalars.append(u)
    }
}
print(out)

03. 順番を保って重複を消す

同じ値の2回目以降を消し、初めて出た順に残します。タグの一覧や、選んだ項目の一覧でよく使います。集合(Set)に入れて戻す書き方では、言語によって順番が変わったり、実行するたびに変わったりします。 ほかの言語・正解の例・解説 →

unique.swift6.3.3 で 9 / 9 件の出力が正解と一致

// 受け取った文字列から、2回目以降に出たものを消し、初めて出た順に出す
let items = Array(CommandLine.arguments.dropFirst())
// Swift の String の == と Set<String> は、見た目が同じ(正規化して同じ)なら同じとみなす。
// 「が」1文字と「か」+濁点を別にするため、UTF-8 のバイトの並びで覚える
var seen = Set<[UInt8]>()
let out = items.filter { seen.insert(Array($0.utf8)).inserted }
for s in out {
    print(s.isEmpty ? "(空)" : s)
}
print("\(out.count)件(\(items.count - out.count)件を消した)")

04. 日付と時刻を ISO 8601 で読み書きする

API やログでよく使う 2026-10-30T09:00:00+09:00 の形を読み、UTC と日本時間で書き直します。標準のパーサーにそのまま渡すと、2月30日を3月2日として読んだり、タイムゾーンの無い日時を動かす場所の時間帯で読んだりします。 ほかの言語・正解の例・解説 →

iso8601.swift6.3.3 で 15 / 15 件の出力が正解と一致

// ISO 8601 の日時を受け取って、UTC と日本時間で書き出す
import Foundation

// ISO8601DateFormatter は秒の小数を「有り」か「無し」のどちらかに決めないと読めず、桁数の扱いも Mac と Linux で揃わない。
// 形は正規表現で決めて、数字を取り出して Calendar で組み立てる。
// /…/ と書く正規表現はコンパイラの設定(Swift 5 モード)で使えないことがあるので、#/…/# で書く
let iso = #/([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})?/#

func convert(_ s: String) -> String {
    // wholeMatch は文字列全体が合うときだけ当たる
    guard let m = s.wholeMatch(of: iso) else { return "NG: 日時の形ではありません" }
    let (y, mo, d, h, mi, se) = (Int(m.1)!, Int(m.2)!, Int(m.3)!, Int(m.4)!, Int(m.5)!, Int(m.6)!)
    // Calendar は動かす場所の設定(Calendar.current)を使わず、西暦の暦と UTC に固定する。
    // TimeZone(secondsFromGMT:) は時差だけのタイムゾーンで、tzdata を見ない
    var cal = Calendar(identifier: .gregorian)
    cal.timeZone = TimeZone(secondsFromGMT: 0)!
    // date(from:) は 2月30日を3月2日に繰り上げるので、作った日時を年月日に戻して入力どおりかを確かめる
    let local = DateComponents(year: y, month: mo, day: d, hour: h, minute: mi, second: se)
    guard let lt = cal.date(from: local),
          cal.dateComponents([.year, .month, .day, .hour, .minute, .second], from: lt) == local
    else { return "NG: 日時の形ではありません" }
    guard let tz = m.8 else { return "NG: タイムゾーン(Z か +09:00 など)を付けてください" }
    var offset = 0
    if tz != "Z" {
        let oh = Int(tz.dropFirst().prefix(2))!, om = Int(tz.suffix(2))!
        if oh > 23 || om > 59 { return "NG: 日時の形ではありません" }
        offset = (tz.first == "+" ? 1 : -1) * (oh * 3600 + om * 60)
    }
    // 書いてある時刻を UTC として作ったので、時差を引くと本当の UTC になる
    let t = lt.addingTimeInterval(TimeInterval(-offset))
    func show(_ format: String, _ secondsFromGMT: Int) -> String {
        let f = DateFormatter()
        f.calendar = cal
        f.locale = Locale(identifier: "en_US_POSIX")
        f.timeZone = TimeZone(secondsFromGMT: secondsFromGMT)!
        f.dateFormat = format
        return f.string(from: t)
    }
    return "UTC " + show("yyyy-MM-dd'T'HH:mm:ss'Z'", 0) + "\n" + "日本時間 " + show("yyyy年MM月dd日 HH:mm:ss", 9 * 3600)
}

print(convert(CommandLine.arguments[1]))

05. アクセスログを集計する

Web サーバーのアクセスログを標準入力から読み、ステータスごとの件数と、応答の遅いリクエストの上位3件を出します。空白で区切って何番目かを取る書き方は手軽ですが、壊れた行を数えたり、応答秒を文字のまま並べたりします。 ほかの言語・正解の例・解説 →

access_log.swift6.3.3 で 8 / 8 件の出力が正解と一致

// 標準入力のアクセスログを読んで、件数・ステータスごとの件数・遅い順の上位3件を出す

// 日時やHTTPの版には空白が入るので、空白で区切って何番目と数えず、行の形そのものを正規表現で確かめる。
// Swift の正規表現の \d は全角などの数字にも合うので [0-9] と書く。#/…/# は「/」を書きやすくする囲み方
let log = #/\S+ \S+ \S+ \[[^\]]+\] "(\S+) (\S+) [^"]+" ([0-9]{3}) ([0-9]+|-) ([0-9]+\.[0-9]+)/#

var rows: [(sec: Substring, secNum: Double, method: Substring, path: Substring, status: Substring)] = []
var bad = 0
// readLine は \n でも \r\n でも行を区切り、行の終わりの記号を取って返す
while let line = readLine() {
    if line.isEmpty {
        continue
    }
    // firstMatch は行のどこかに合えば通る。行全体が形どおりか確かめるのは wholeMatch
    guard let m = try? log.wholeMatch(in: line) else {
        bad += 1
        continue
    }
    // 正規表現で「数字.数字」と確かめてあるので、Double(…) は nil にならない
    rows.append((m.5, Double(m.5)!, m.1, m.2, m.3))
}

print("件数 \(rows.count)")
if bad > 0 {
    print("読めない行 \(bad)")
}

var counts: [Substring: Int] = [:]
for r in rows {
    counts[r.status, default: 0] += 1
}
print("ステータス")
// Dictionary を回す順番は決まっておらず、動かすたびに変わる。キーを並べてから出す
for status in counts.keys.sorted() {
    print("\(status) \(counts[status]!)")
}

print("遅い順")
// Swift の sort は安定と約束されていない(いまの実装は安定だが、文書では保証しない)。
// 何行目かを2つ目のキーにして、同じ秒なら先に出た行を先にする
let slow = rows.enumerated().sorted { a, b in
    a.element.secNum != b.element.secNum ? a.element.secNum > b.element.secNum : a.offset < b.offset
}.prefix(3)
for (_, r) in slow {
    print("\(r.sec) \(r.method) \(r.path)")
}

06. URL 用の文字列(slug)を作る

記事のタイトルから「hello-world」のような URL の一部を作ります。ブログや商品ページのアドレスを、見て意味が分かる形にするときに使います。よく見かける書き方は、日本語のタイトルをそのまま URL に入れたり、空の slug を黙って返したりします。 ほかの言語・正解の例・解説 →

slug.swift6.3.3 で 14 / 14 件の出力が正解と一致

// 文字列を URL に使える形(slug)に変える
let maxLen = 32

// lowercased() は Unicode の決まりで変えるので、İ が2文字になる。1文字ずつ見て ASCII だけを変える
var s = ""
for ch in CommandLine.arguments[1] {
    // isLetter は「あ」にも true を返す。isASCII と合わせて半角の英数字だけを残す
    let c = ch.isASCII && ch.isUppercase ? Character(ch.lowercased()) : ch
    if c.isASCII && (c.isLowercase || c.isNumber) {
        s.append(c)
    } else if !s.hasSuffix("-") {
        // 続いている分はまとめて「-」1つにする(絵文字のように複数の値でできた文字も1つ)
        s += "-"
    }
}
if s.hasPrefix("-") { s.removeFirst() }
if s.hasSuffix("-") { s.removeLast() }
if s.count > maxLen {
    s = String(s.prefix(maxLen))
    if s.hasSuffix("-") { s.removeLast() }
}
print(s.isEmpty ? "NG: URL に使える文字がありません" : s)

07. URL エンコードする

検索語や日本語の名前を、URL のクエリの値(`?q=...`)に入れられる形に変えます。よく見かける書き方は、空白を「+」にしたり、`!` や `~` を変えずに残したりして、受け取る側で別の文字列になります。 ほかの言語・正解の例・解説 →

url_encode.swift6.3.3 で 15 / 15 件の出力が正解と一致

// 文字列を URL のクエリの値に入れられる形(パーセントエンコード)に変える
// addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) は & や = を残す(Foundation も要る)
// utf8 はそのまま UTF-8 のバイトの並び
let keep = Set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~".utf8)
let hex = Array("0123456789ABCDEF")

var out = ""
for b in CommandLine.arguments[1].utf8 {
    if keep.contains(b) {
        out.append(Character(UnicodeScalar(b)))
    } else {
        out.append("%")
        out.append(hex[Int(b >> 4)])
        out.append(hex[Int(b & 0xF)])
    }
}
print(out)

ほかの言語