01. スネークケースとキャメルケースを変換する
user_id と userId を行き来させます。データベースの列名と JSON のキー、定数と変数のように、名前の書き方をそろえるときに使います。HTTPServer のように大文字が続く名前で、よく見かける書き方はほとんどの言語で崩れます。 ほかの言語・正解の例・解説 →
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);
}
02. HTML の特殊文字をエスケープする
利用者が入力した文字を HTML に埋め込む前に、< > & " ' を < などに置き換えます。標準の関数は言語ごとに ' の書き方が違い、この決まりの形とそのまま一致したのは Ruby だけでした。 ほかの言語・正解の例・解説 →
escape.rs1.99.0 で 8 / 8 件の出力が正解と一致
// 受け取った文字列の & < > " ' を、HTML の書き方(& < > " ')に変える
// Rust の標準ライブラリにはエスケープの関数がないので、1文字ずつ見て置き換える
fn escape(s: &str) -> String {
// 変えると長くなるので、少し多めに場所を取っておく
let mut out = String::with_capacity(s.len() + s.len() / 8);
// chars は1文字(Unicode のコードポイント)ずつ取り出すので、日本語や絵文字が途中で切れない
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
out
}
fn main() {
let args: Vec<String> = std::env::args().collect();
println!("{}", escape(&args[1]));
}
03. 順番を保って重複を消す
同じ値の2回目以降を消し、初めて出た順に残します。タグの一覧や、選んだ項目の一覧でよく使います。集合(Set)に入れて戻す書き方では、言語によって順番が変わったり、実行するたびに変わったりします。 ほかの言語・正解の例・解説 →
unique.rs1.99.0 で 9 / 9 件の出力が正解と一致
// 受け取った文字列から、2回目以降に出たものを消し、初めて出た順に出す
use std::collections::HashSet;
fn main() {
let items: Vec<String> = std::env::args().skip(1).collect();
// dedup は隣り合う重複しか消さないので、sort してから使うと並びが変わる。
// HashSet の insert は初めて入れたときだけ true を返すので、retain で初めてのものだけ残す
let mut seen = HashSet::new();
let mut out = items.clone();
out.retain(|s| seen.insert(s.clone()));
for s in &out {
println!("{}", if s.is_empty() { "(空)" } else { s });
}
println!("{}件({}件を消した)", out.len(), items.len() - out.len());
}
04. 日付と時刻を ISO 8601 で読み書きする
API やログでよく使う 2026-10-30T09:00:00+09:00 の形を読み、UTC と日本時間で書き直します。標準のパーサーにそのまま渡すと、2月30日を3月2日として読んだり、タイムゾーンの無い日時を動かす場所の時間帯で読んだりします。 ほかの言語・正解の例・解説 →
iso8601.rs1.99.0 で 15 / 15 件の出力が正解と一致
// ISO 8601 の日時を受け取って、UTC と日本時間で書き出す
// Rust の標準ライブラリには日時の型も正規表現も無い。形は1文字ずつ確かめ、日時は「1970-01-01 からの秒数」にして計算する
// (SystemTime は「いま」を測るための型で、年月日との行き来はできない)
// 4で割り切れる年はうるう年。ただし100で割り切れる年は違い、400で割り切れる年はうるう年
fn is_leap(y: i64) -> bool {
(y % 4 == 0 && y % 100 != 0) || y % 400 == 0
}
fn days_in_month(y: i64, m: i64) -> i64 {
match m {
2 => if is_leap(y) { 29 } else { 28 },
4 | 6 | 9 | 11 => 30,
_ => 31,
}
}
// 1970-01-01 から数えて何日目か。年ごと・月ごとの日数を足していく(1970年より前は引く)
fn days_from_epoch(y: i64, m: i64, d: i64) -> i64 {
let mut days = 0;
if y >= 1970 {
for yy in 1970..y { days += if is_leap(yy) { 366 } else { 365 }; }
} else {
for yy in y..1970 { days -= if is_leap(yy) { 366 } else { 365 }; }
}
for mm in 1..m { days += days_in_month(y, mm); }
days + d - 1
}
// 秒数から年月日と時分秒に戻す。日数を年・月の長さで順に引いて、月末・年末の繰り上がりを作る
fn from_epoch(secs: i64) -> (i64, i64, i64, i64, i64, i64) {
// 負の数の割り算は 0 に向かって丸めるので、日と秒への分け方は div_euclid / rem_euclid で書く
let (mut days, rest) = (secs.div_euclid(86400), secs.rem_euclid(86400));
let mut y = 1970;
while days < 0 { y -= 1; days += if is_leap(y) { 366 } else { 365 }; }
while days >= if is_leap(y) { 366 } else { 365 } { days -= if is_leap(y) { 366 } else { 365 }; y += 1; }
let mut m = 1;
while days >= days_in_month(y, m) { days -= days_in_month(y, m); m += 1; }
(y, m, days + 1, rest / 3600, rest / 60 % 60, rest % 60)
}
// 数字だけの並びを整数にする。&str の parse は「+9」のような符号も通すので、先に1文字ずつ数字か確かめる
fn num(b: &[u8]) -> Option<i64> {
if b.is_empty() || !b.iter().all(|c| c.is_ascii_digit()) { return None; }
Some(b.iter().fold(0, |n, c| n * 10 + (c - b'0') as i64))
}
fn convert(s: &str) -> Result<String, &'static str> {
const FORM: &str = "NG: 日時の形ではありません";
// 位置で切り出すので、バイト列として見る(日本語が混ざっても添字の位置で落ちない)
let b = s.as_bytes();
if b.len() < 19 || b[4] != b'-' || b[7] != b'-' || b[10] != b'T' || b[13] != b':' || b[16] != b':' { return Err(FORM); }
let f = |i: usize, j: usize| num(&b[i..j]).ok_or(FORM);
let (y, mo, d, h, mi, se) = (f(0, 4)?, f(5, 7)?, f(8, 10)?, f(11, 13)?, f(14, 16)?, f(17, 19)?);
let mut rest = &b[19..];
// 秒の小数は1〜9桁。切り捨てるので、数字か確かめて読み飛ばす
if rest.first() == Some(&b'.') {
let n = rest[1..].iter().take_while(|c| c.is_ascii_digit()).count();
if n < 1 || n > 9 { return Err(FORM); }
rest = &rest[1 + n..];
}
// タイムゾーンは無し・Z・±HH:MM のどれか。ほかの形はここで弾く
let offset = match rest {
[] => None,
[b'Z'] => Some(0),
[sign @ (b'+' | b'-'), _, _, b':', _, _] => {
let (oh, om) = (num(&rest[1..3]).ok_or(FORM)?, num(&rest[4..6]).ok_or(FORM)?);
if oh > 23 || om > 59 { return Err(FORM); }
Some(if *sign == b'+' { 1 } else { -1 } * (oh * 3600 + om * 60))
}
_ => return Err(FORM),
};
// 2月30日・25時は、月の日数と時刻の範囲と比べて見分ける
if mo < 1 || mo > 12 || d < 1 || d > days_in_month(y, mo) || h > 23 || mi > 59 || se > 59 { return Err(FORM); }
let offset = offset.ok_or("NG: タイムゾーン(Z か +09:00 など)を付けてください")?;
// 書いてある時刻から時差を引くと UTC。日本時間は UTC に9時間足す
let utc = days_from_epoch(y, mo, d) * 86400 + h * 3600 + mi * 60 + se - offset;
let (uy, um, ud, uh, umi, us) = from_epoch(utc);
let (jy, jm, jd, jh, jmi, js) = from_epoch(utc + 9 * 3600);
Ok(format!("UTC {:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z\n日本時間 {:04}年{:02}月{:02}日 {:02}:{:02}:{:02}",
uy, um, ud, uh, umi, us, jy, jm, jd, jh, jmi, js))
}
fn main() {
let s = std::env::args().nth(1).unwrap_or_default();
match convert(&s) {
Ok(out) => println!("{}", out),
Err(msg) => println!("{}", msg),
}
}
05. アクセスログを集計する
Web サーバーのアクセスログを標準入力から読み、ステータスごとの件数と、応答の遅いリクエストの上位3件を出します。空白で区切って何番目かを取る書き方は手軽ですが、壊れた行を数えたり、応答秒を文字のまま並べたりします。 ほかの言語・正解の例・解説 →
access_log.rs1.99.0 で 8 / 8 件の出力が正解と一致
// 標準入力のアクセスログを読んで、件数・ステータスごとの件数・遅い順の上位3件を出す
use std::collections::BTreeMap;
use std::io::BufRead;
struct Row {
sec: String,
sec_num: f64,
method: String,
path: String,
status: String,
}
// 空でなく、空白を含まない(正規表現の \S+ にあたる)
fn is_token(s: &str) -> bool {
!s.is_empty() && !s.chars().any(char::is_whitespace)
}
fn is_digits(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
}
// Rust の標準には正規表現が無いので、決まった区切り(「 [」「] "」「"」「 」)で順に切り分けて形を確かめる。
// 日時やHTTPの版には空白が入るので、行をまるごと空白で区切って何番目と数えることはしない
fn parse(line: &str) -> Option<Row> {
// <IP> - - [<日時>] "<リクエスト>" <ステータス> <バイト数> <応答秒>
let (head, rest) = line.split_once(" [")?;
let head: Vec<&str> = head.split(' ').collect();
if head.len() != 3 || !head.iter().all(|t| is_token(t)) {
return None;
}
let (date, rest) = rest.split_once("] \"")?;
if date.is_empty() || date.contains(']') {
return None;
}
// リクエストの中には「"」が無い。次の「"」までがリクエスト
let (request, tail) = rest.split_once('"')?;
let mut req = request.splitn(3, ' ');
let method = req.next()?;
let path = req.next()?;
let version = req.next()?;
if !is_token(method) || !is_token(path) || version.is_empty() {
return None;
}
let tail: Vec<&str> = tail.strip_prefix(' ')?.split(' ').collect();
let [status, bytes, sec] = tail[..] else { return None };
if status.len() != 3 || !is_digits(status) {
return None;
}
if bytes != "-" && !is_digits(bytes) {
return None;
}
// parse::<f64>() は "1e3" や "inf" も受け付けるので、「数字.数字」の形は自分で確かめる
let (int_part, frac_part) = sec.split_once('.')?;
if !is_digits(int_part) || !is_digits(frac_part) {
return None;
}
Some(Row {
sec: sec.to_string(),
sec_num: sec.parse().ok()?,
method: method.to_string(),
path: path.to_string(),
status: status.to_string(),
})
}
fn main() {
let mut rows = Vec::new();
let mut bad = 0;
// lines() は \n でも \r\n でも行を区切り、行の終わりの記号を取って返す
for line in std::io::stdin().lock().lines() {
let line = line.unwrap();
if line.is_empty() {
continue;
}
match parse(&line) {
Some(r) => rows.push(r),
None => bad += 1,
}
}
println!("件数 {}", rows.len());
if bad > 0 {
println!("読めない行 {}", bad);
}
// HashMap は回す順番が動かすたびに変わる。BTreeMap はキーの小さい順に回る
let mut counts: BTreeMap<&str, usize> = BTreeMap::new();
for r in &rows {
*counts.entry(&r.status).or_insert(0) += 1;
}
println!("ステータス");
for (status, n) in &counts {
println!("{} {}", status, n);
}
println!("遅い順");
// f64 は NaN があるので Ord ではなく、sort_by_key には渡せない。total_cmp で比べる。
// sort_by は安定(同じ秒なら先に出た行が先)。sort_unstable_by は安定でない
let mut slow: Vec<&Row> = rows.iter().collect();
slow.sort_by(|a, b| b.sec_num.total_cmp(&a.sec_num));
for r in slow.iter().take(3) {
println!("{} {} {}", r.sec, r.method, r.path);
}
}
06. URL 用の文字列(slug)を作る
記事のタイトルから「hello-world」のような URL の一部を作ります。ブログや商品ページのアドレスを、見て意味が分かる形にするときに使います。よく見かける書き方は、日本語のタイトルをそのまま URL に入れたり、空の slug を黙って返したりします。 ほかの言語・正解の例・解説 →
slug.rs1.99.0 で 14 / 14 件の出力が正解と一致
// 文字列を URL に使える形(slug)に変える
const MAX: usize = 32;
fn main() {
let args: Vec<String> = std::env::args().collect();
// 標準に正規表現が無いので1文字ずつ見る。to_lowercase() は Unicode の決まり(İ が2文字)なので使わない
let mut s = String::new();
for c in args[1].chars() {
let c = c.to_ascii_lowercase();
if c.is_ascii_lowercase() || c.is_ascii_digit() {
s.push(c);
} else if !s.ends_with('-') {
// 続いている分はまとめて「-」1つにする
s.push('-');
}
}
let mut s = s.trim_matches('-').to_string();
// ここまでで半角の英数字と「-」だけになっているので、len(バイト数)=文字数
if s.len() > MAX {
s = s[..MAX].trim_end_matches('-').to_string();
}
println!("{}", if s.is_empty() { "NG: URL に使える文字がありません" } else { &s });
}
07. URL エンコードする
検索語や日本語の名前を、URL のクエリの値(`?q=...`)に入れられる形に変えます。よく見かける書き方は、空白を「+」にしたり、`!` や `~` を変えずに残したりして、受け取る側で別の文字列になります。 ほかの言語・正解の例・解説 →
url_encode.rs1.99.0 で 15 / 15 件の出力が正解と一致
// 文字列を URL のクエリの値に入れられる形(パーセントエンコード)に変える
fn main() {
let args: Vec<String> = std::env::args().collect();
// 標準に URL の関数が無いので自分で書く。String の as_bytes はそのまま UTF-8 のバイトの並び
let mut out = String::new();
for &b in args[1].as_bytes() {
let c = b as char;
if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '~') {
out.push(c);
} else {
out.push_str(&format!("%{:02X}", b));
}
}
println!("{}", out);
}