画面にこう出れば正解
どの言語で書いても、同じ入力なら画面に出る文字は1文字も違わない、というのが正解の決め方です(例は Python で動かしたときのコマンドです)。
基本
$ python3 email.py taro@example.com
OK: taro@example.com記号と2段のドメイン
$ python3 email.py taro.yamada+news@example.co.jp
OK: taro.yamada+news@example.co.jpドメインに点がない
$ python3 email.py taro@example
NG: メールアドレスの形ではありません@が2つ
$ python3 email.py taro@@example.com
NG: メールアドレスの形ではありません@の前が空
$ python3 email.py @example.com
NG: メールアドレスの形ではありません日本語
$ python3 email.py たろう@example.com
NG: メールアドレスの形ではありません最後に改行
$ python3 email.py "taro@example.com
"
NG: メールアドレスの形ではありません2行目にアドレス
$ python3 email.py "hello
taro@example.com"
NG: メールアドレスの形ではありません前に空白
$ python3 email.py " taro@example.com"
NG: メールアドレスの形ではありません点が続く
$ python3 email.py taro..yamada@example.com
NG: メールアドレスの形ではありません全角の@
$ python3 email.py taro@example.com
NG: メールアドレスの形ではありません10言語のプログラム
どれもそのまま保存して動かせます。毎朝、上の正解の例をすべて実際に動かして確かめています。
email.php8.4.26 で 11 / 11 件の出力が正解と一致
<?php
// 受け取った文字列が、メールアドレスの形かを確かめる
$s = $argv[1];
// ローカル部は「.」で区切った、英数字と _ + - の並び(先頭・最後の「.」と「..」を通さない)。
// ドメインは、英数字で始まり英数字で終わる部分を「.」でつなぎ、最後は英字2文字以上
$local = '[A-Za-z0-9_+-]+(\.[A-Za-z0-9_+-]+)*';
$label = '[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?';
// 「$」は最後の改行の直前にも合うので、文字列の本当の最後を表す「\z」を使う
if (preg_match('/\A' . $local . '@(' . $label . '\.)+[A-Za-z]{2,}\z/', $s)) {
echo "OK: " . $s . "\n";
} else {
echo "NG: メールアドレスの形ではありません\n";
}
動かし方(ファイルを保存したフォルダで) ・ PHP を入れる
$ php email.php taro@example.com
email.py3.14.7 で 11 / 11 件の出力が正解と一致
# 受け取った文字列が、メールアドレスの形かを確かめる
import re
import sys
s = sys.argv[1]
# ローカル部は「.」で区切った、英数字と _ + - の並び(先頭・最後の「.」と「..」を通さない)。
# ドメインは、英数字で始まり英数字で終わる部分を「.」でつなぎ、最後は英字2文字以上
LOCAL = r"[A-Za-z0-9_+-]+(\.[A-Za-z0-9_+-]+)*"
LABEL = r"[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?"
# 「\w」は日本語にも合い、「$」は最後の改行の直前にも合うので、文字を書き出し、文字列全体に合うかを fullmatch で調べる
if re.fullmatch(LOCAL + "@(" + LABEL + r"\.)+[A-Za-z]{2,}", s):
print("OK: " + s)
else:
print("NG: メールアドレスの形ではありません")
動かし方(ファイルを保存したフォルダで) ・ Python を入れる
$ python3 email.py taro@example.com
email.go1.27.2 で 11 / 11 件の出力が正解と一致
// 受け取った文字列が、メールアドレスの形かを確かめる
package main
import (
"fmt"
"os"
"regexp"
)
// ローカル部は「.」で区切った、英数字と _ + - の並び(先頭・最後の「.」と「..」を通さない)。
// ドメインは、英数字で始まり英数字で終わる部分を「.」でつなぎ、最後は英字2文字以上
const local = `[A-Za-z0-9_+-]+(\.[A-Za-z0-9_+-]+)*`
const label = `[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?`
// Go の「^」「$」は、何も指定しなければ文字列の先頭と最後だけに合う
var email = regexp.MustCompile(`^` + local + `@(` + label + `\.)+[A-Za-z]{2,}$`)
func main() {
s := os.Args[1]
if email.MatchString(s) {
fmt.Println("OK: " + s)
} else {
fmt.Println("NG: メールアドレスの形ではありません")
}
}
動かし方(ファイルを保存したフォルダで) ・ Go を入れる
$ go run email.go taro@example.com
email.js24.15.0 で 11 / 11 件の出力が正解と一致
// 受け取った文字列が、メールアドレスの形かを確かめる
const s = process.argv[2];
// ローカル部は「.」で区切った、英数字と _ + - の並び(先頭・最後の「.」と「..」を通さない)。
// ドメインは、英数字で始まり英数字で終わる部分を「.」でつなぎ、最後は英字2文字以上
// JavaScript の「^」「$」は、m フラグを付けなければ文字列の先頭と最後だけに合う
const email = /^[A-Za-z0-9_+-]+(\.[A-Za-z0-9_+-]+)*@([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?\.)+[A-Za-z]{2,}$/;
if (email.test(s)) {
console.log("OK: " + s);
} else {
console.log("NG: メールアドレスの形ではありません");
}
動かし方(ファイルを保存したフォルダで) ・ JavaScript を入れる
$ node email.js taro@example.com
Email.java21.0.12 で 11 / 11 件の出力が正解と一致
// 受け取った文字列が、メールアドレスの形(この回の決まり)かを確かめる
public class Email {
public static void main(String[] args) {
String s = args[0];
// matches は文字列全体が合うかを調べる(find と $ だと、最後の改行の前でも合ってしまう)。
// \w ではなく、使ってよい文字を [A-Za-z0-9...] と書く。「.」は文字のかたまりの間にだけ置ける
String local = "[A-Za-z0-9_+-]+(\\.[A-Za-z0-9_+-]+)*";
String label = "[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?";
if (s.matches(local + "@(" + label + "\\.)+[A-Za-z]{2,}")) {
System.out.println("OK: " + s);
} else {
System.out.println("NG: メールアドレスの形ではありません");
}
}
}
動かし方(ファイルを保存したフォルダで) ・ Java を入れる
$ java Email.java taro@example.com
email.cs10.0.402 で 11 / 11 件の出力が正解と一致
// 受け取った文字列が、メールアドレスの形(この回の決まり)かを確かめる
using System.Text.RegularExpressions;
string s = args[0];
// $ は最後の改行の前にも合い、\w はひらがなにも合うので、\A と \z で挟み、使ってよい文字を [A-Za-z0-9...] と書く。
// 「.」は文字のかたまりの間にだけ置ける
string local = @"[A-Za-z0-9_+-]+(\.[A-Za-z0-9_+-]+)*";
string label = @"[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?";
if (Regex.IsMatch(s, $@"\A{local}@({label}\.)+[A-Za-z]{{2,}}\z"))
{
Console.WriteLine($"OK: {s}");
}
else
{
Console.WriteLine("NG: メールアドレスの形ではありません");
}
動かし方(ファイルを保存したフォルダで) ・ C# を入れる
$ dotnet run email.cs -- taro@example.com
email.rs1.99.0 で 11 / 11 件の出力が正解と一致
// 受け取った文字列が、メールアドレスの形(この回の決まり)かを確かめる
// Rust の標準ライブラリには正規表現がないので、決まりを1つずつ確かめる
// ローカル部: 半角の英数字と「. _ + -」だけ。先頭と最後は「.」でない。「..」と続かない
fn is_local(s: &str) -> bool {
!s.is_empty()
&& s.chars().all(|c| c.is_ascii_alphanumeric() || "._+-".contains(c))
&& !s.starts_with('.')
&& !s.ends_with('.')
&& !s.contains("..")
}
// ドメインの各部分: 半角の英数字と「-」だけ。先頭と最後は「-」でない
fn is_label(s: &str) -> bool {
!s.is_empty()
&& s.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
&& !s.starts_with('-')
&& !s.ends_with('-')
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let s = &args[1];
// 「@」がちょうど1つなら、区切ると2つに分かれる
let parts: Vec<&str> = s.split('@').collect();
let ok = parts.len() == 2 && is_local(parts[0]) && {
let labels: Vec<&str> = parts[1].split('.').collect();
let last = labels[labels.len() - 1];
// ドメインは2つ以上の部分。最後の部分は英字2文字以上
labels.len() >= 2
&& labels.iter().all(|l| is_label(l))
&& last.len() >= 2
&& last.chars().all(|c| c.is_ascii_alphabetic())
};
if ok {
println!("OK: {}", s);
} else {
println!("NG: メールアドレスの形ではありません");
}
}
動かし方(ファイルを保存したフォルダで) ・ Rust を入れる
$ rustc email.rs && ./email taro@example.com
email.rb4.0.7 で 11 / 11 件の出力が正解と一致
# 受け取った文字列が、メールアドレスの形かを確かめる
s = ARGV[0]
# ローカル部は「.」で区切った、英数字と _ + - の並び(先頭・最後の「.」と「..」を通さない)。
# ドメインは、英数字で始まり英数字で終わる部分を「.」でつなぎ、最後は英字2文字以上
LOCAL = /[A-Za-z0-9_+-]+(\.[A-Za-z0-9_+-]+)*/
LABEL = /[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?/
# Ruby の「^」「$」は行の先頭・行の最後なので、文字列の先頭「\A」と最後「\z」を使う
EMAIL = /\A#{LOCAL}@(#{LABEL}\.)+[A-Za-z]{2,}\z/
if EMAIL.match?(s)
puts "OK: " + s
else
puts "NG: メールアドレスの形ではありません"
end
動かし方(ファイルを保存したフォルダで) ・ Ruby を入れる
$ ruby email.rb taro@example.com
email.kt2.4.21 で 11 / 11 件の出力が正解と一致
// 受け取った文字列が、メールアドレスの形(この回の決まり)かを確かめる
fun main(args: Array<String>) {
val s = args[0]
// matches は文字列全体が合うかを調べる(containsMatchIn と $ だと、最後の改行の前でも合ってしまう)。
// \w ではなく、使ってよい文字を [A-Za-z0-9...] と書く。「.」は文字のかたまりの間にだけ置ける
val local = """[A-Za-z0-9_+-]+(\.[A-Za-z0-9_+-]+)*"""
val label = """[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?"""
if (Regex("""$local@($label\.)+[A-Za-z]{2,}""").matches(s)) {
println("OK: $s")
} else {
println("NG: メールアドレスの形ではありません")
}
}
動かし方(ファイルを保存したフォルダで) ・ Kotlin を入れる
$ kotlinc email.kt -include-runtime -d email.jar && java -jar email.jar taro@example.com
email.swift6.3.3 で 11 / 11 件の出力が正解と一致
// 受け取った文字列が、メールアドレスの形(この回の決まり)かを確かめる
let s = CommandLine.arguments[1]
// wholeMatch は文字列全体が合うかを調べる。\w はひらがなにも合うので、使ってよい文字を [A-Za-z0-9...] と書く。
// 「.」は文字のかたまりの間にだけ置ける
let local = #"[A-Za-z0-9_+-]+(\.[A-Za-z0-9_+-]+)*"#
let label = #"[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?"#
let pattern = try! Regex(local + "@(" + label + #"\.)+[A-Za-z]{2,}"#)
if s.wholeMatch(of: pattern) != nil {
print("OK: \(s)")
} else {
print("NG: メールアドレスの形ではありません")
}
動かし方(ファイルを保存したフォルダで) ・ Swift を入れる
$ swift email.swift taro@example.com
言語でつまずきやすいところ
初心者がよく書く形(各言語で最初に習う書き方をそのまま使ったもの)を、同じ正解表で動かした結果です。言語を出した答えごとにまとめ、赤い字はその言語が実際に出した間違った行です。
| 入力 | 初心者がよく書く形の答え(10言語) |
|---|---|
基本taro@example.com | ✓10言語すべて正解 |
記号と2段のドメインtaro.yamada+news@example.co.jp | ✓10言語すべて正解 |
ドメインに点がないtaro@example | ✓10言語すべて正解 |
@が2つtaro@@example.com |
|
@の前が空@example.com |
|
日本語たろう@example.com |
|
最後に改行"taro@example.com
" |
|
2行目にアドレス"hello
taro@example.com" |
|
前に空白" taro@example.com" |
|
点が続くtaro..yamada@example.com |
|
全角の@taro@example.com | ✓10言語すべて正解 |
この回でやること
- 正規表現で、文字列全体の形を確かめる
- 改行や空白が混ざった入力を、通さない
この回の決まり
この回では、よく使われる形に絞った、次の決まりにします(本物のメールアドレスの決まりは、もっと複雑です)。
- 「@」がちょうど1つ。前(ローカル部)と後ろ(ドメイン)に分ける
- ローカル部: 半角の英数字と「. _ + -」だけ。先頭と最後は「.」でない。「..」と続かない
- ドメイン: 「.」で区切った2つ以上の部分。各部分は半角の英数字と「-」だけで、先頭と最後は「-」でない。最後の部分は英字2文字以上
- 前後の空白や改行も含めて、文字列全体がこの形でなければ NG
解説
「文字列全体」を確かめる
正規表現は、何も指定しなければ「どこかに合う部分があるか」を探します。文字列全体を確かめるには、先頭の「^」と最後の「$」を付けるか、全体に合うかを調べる関数を使います。
「$」は、最後の改行の前にも合うことがある
初心者がよく書く正規表現 ^[\w.+-]+@[\w-]+\.[\w.-]+$ を、各言語の普通の一致の関数で試しました。
| 入力 | 通してしまった言語 |
|---|---|
taro@example.com の最後に改行 | PHP・Python・Ruby・Java・Kotlin・C#・Swift(Linux)。「$」が最後の改行の直前にも合う |
1行目が hello、2行目に taro@example.com | Ruby。「^」と「$」が行の先頭・行の最後を意味する |
たろう@example.com | Python・C#・Swift。「\w」がひらがなにも合う |
taro..yamada@example.com | 正規表現を使った9言語すべて。[\w.+-]+ は点が続いても合う(正規表現そのものの穴) |
Go と JavaScript の「$」は文字列の最後にだけ合うので、改行のケースは通しませんでした。Swift の range(of:options: .regularExpression) は、Mac では改行のケースを通さず、Linux では通しました。同じプログラムでも、動かす場所で結果が変わります。Mac でも NSRegularExpression で同じ正規表現を試すと改行のケースに合ったので、違いは正規表現そのものではなく、Mac の range(of:options:) の動きです。\z を使えば、Mac でも Linux でも同じ結果になりました。
Rust には標準の正規表現がありません。「@ と . が入っているか」だけで確かめると、11件中4件しか合いませんでした。
文字列全体を確かめるには、全体に合うかを調べる関数(Python re.fullmatch・Java matches・Kotlin matches・Swift wholeMatch)を使うか、「文字列の最初と最後」だけを意味する \A と \z を使います。
「\w」は、日本語にも合うことがある
「\w」(英数字と _)は、言語によっては、ひらがなや漢字にも合います(上の表では Python・C#・Swift)。第7回 の「\d」と同じ問題です。この回では、使ってよい文字を [A-Za-z0-9._+-] のように書いて決めています。
練習
- ドメインを「example.com」だけに限ってみましょう
- NG のとき、どこが違うのか(@ がない、ドメインに点がない、など)を出し分けてみましょう