練習問題+自動採点HTM
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>正規表現 練習問題(自動採点)</title>
<style>
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,"Hiragino Kaku Gothic ProN","Noto Sans JP",sans-serif;margin:20px;color:#111}
h1{font-size:1.6rem;margin-bottom:0.2rem}
p.lead{margin-top:0;color:#444}
.exercise{border:1px solid #e6e6e6;padding:12px;border-radius:8px;margin:12px 0;background:#fafafa}
label{display:block;font-weight:600;margin-bottom:6px}
input[type=text], textarea{width:100%;padding:8px;border-radius:6px;border:1px solid #ddd;font-size:0.95rem}
.row{display:flex;gap:8px}
.small{width:140px}
button{background:#0b74de;color:white;padding:8px 12px;border-radius:8px;border:0;cursor:pointer}
button.ghost{background:transparent;color:#0b74de;border:1px solid #0b74de}
.results{margin-top:8px;padding:8px;border-radius:6px;background:#fff}
.ok{color:green;font-weight:700}
.ng{color:#c0392b;font-weight:700}
.score{font-size:1.2rem;margin-top:10px}
.hint{font-size:0.9rem;color:#555;margin-top:6px}
pre{background:#f7f7f7;padding:8px;border-radius:6px;overflow:auto}
.flex-between{display:flex;justify-content:space-between;align-items:center}
footer{margin-top:18px;color:#666;font-size:0.9rem}
</style>
</head>
<body>
<h1>正規表現 練習問題(自動採点)</h1>
<p class="lead">各問題の入力欄に正規表現(スラッシュ形式でも生文字列形式でも可)を入れて「採点」ボタンを押すと、自動的に採点します。エラー時はメッセージを表示します。</p>
<div id="exercises">
<!-- 問題1 -->
<div class="exercise" data-id="1">
<div class="flex-between">
<div>
<strong>問題 1(基本)</strong>
<div class="hint">文字列が <code>0〜9 の数字だけ</code> で構成されているか判定する正規表現を作ってください。</div>
</div>
<div>配点: 10</div>
</div>
<label>あなたの正規表現</label>
<input type="text" class="pattern" placeholder="例: ^\d+$ または /^(?:\d+)$/" />
<div class="results"></div>
</div>
<!-- 問題2 -->
<div class="exercise" data-id="2">
<div class="flex-between">
<div>
<strong>問題 2(メール)</strong>
<div class="hint">簡易的なメールアドレス(`文字列@文字列.文字列`)にマッチする正規表現を作ってください(完璧である必要はありません)。</div>
</div>
<div>配点: 15</div>
</div>
<label>あなたの正規表現</label>
<input type="text" class="pattern" placeholder="例: ^[\w.-]+@[\w.-]+\.[A-Za-z]{2,}$" />
<div class="results"></div>
</div>
<!-- 問題3 -->
<div class="exercise" data-id="3">
<div class="flex-between">
<div>
<strong>問題 3(キャプチャ)</strong>
<div class="hint">YYYY-MM-DD 形式の日付文字列から年・月・日をキャプチャしてください。キャプチャした値を使えるようにしてください。</div>
</div>
<div>配点: 20</div>
</div>
<label>あなたの正規表現</label>
<input type="text" class="pattern" placeholder="例: ^(\d{4})-(\d{2})-(\d{2})$" />
<div class="results"></div>
</div>
<!-- 問題4 -->
<div class="exercise" data-id="4">
<div class="flex-between">
<div>
<strong>問題 4(置換)</strong>
<div class="hint">文字列内の連続する空白(スペース・タブなど)を 1 個のスペースに置換する正規表現を書き、置換後の文字列を返してください。置換文字列も入力してください。</div>
</div>
<div>配点: 20</div>
</div>
<label>あなたの正規表現</label>
<input type="text" class="pattern" placeholder="例: /\s+/g または \s+/g" />
<label>置換文字列</label>
<input type="text" class="replacement" placeholder="例: ' '(半角スペース)" />
<div class="results"></div>
</div>
<!-- 問題5 -->
<div class="exercise" data-id="5">
<div class="flex-between">
<div>
<strong>問題 5(応用)</strong>
<div class="hint">文章から先頭が大文字の単語(英字)をすべて抽出してください(例: "Tokyo is Great" → ["Tokyo","Great"])。</div>
</div>
<div>配点: 35</div>
</div>
<label>あなたの正規表現</label>
<input type="text" class="pattern" placeholder="例: /\b[A-Z][a-zA-Z]*\b/g" />
<div class="results"></div>
</div>
</div>
<div style="margin-top:12px;display:flex;gap:8px;">
<button id="grade">採点する</button>
<button id="showSolutions" class="ghost">模範解答を表示</button>
<button id="reset" class="ghost">リセット</button>
</div>
<div class="score" id="scoreArea" role="status" aria-live="polite"></div>
<footer>
<p>注意: この自動採点は教育目的の簡易チェックです。正規表現は設計方法や要件によってベスト解が変わるため、模範解答は一例です。</p>
</footer>
<script>
// テストデータと模範解答(採点用)
const problems = {
1: {
tests: ["0","123","001","", "12a"," 123"],
expected: [true,true,true,false,false,false],
answer: '^\\d+$'
},
2: {
tests: ["a@b.com","user.name-1@domain.co","no-at-symbol","@domain.com","user@.com","user@domain.c"],
expected: [true,true,false,false,false,false],
answer: '^[\\w.-]+@[\\w.-]+\\.[A-Za-z]{2,}$'
},
3: {
tests: ["2025-10-13","1999-01-01","2025-1-1","abcd-ef-gh"],
// expects capture groups: year, month(2 digits), day(2 digits) — match only if all captured properly
expected: [true,true,false,false],
answer: '^(\\d{4})-(\\d{2})-(\\d{2})$'
},
4: {
tests: ["a b","a b","line\t\tend"," no-space "],
expected: ["a b","a b","line end"," no-space "],
answer: '\\s+' , replacement: ' '
},
5: {
tests: ["Tokyo is Great","I love NewYork City","no Capitals here","Apple pie"],
expected: [["Tokyo","Great"],["I","NewYork","City"],[],["Apple"]],
answer: '\\b[A-Z][a-zA-Z]*\\b', flags: 'g'
}
};
// ヘルパー: ユーザー入力から正規表現を作る(/pat/flags の形式か生のパターンどちらも許す)
function parseRegex(input){
input = (input||"").trim();
if(!input) return null;
// 先頭と末尾が / で囲まれている場合(例: /abc/g)
if(input.startsWith('/') && input.lastIndexOf('/')>0){
const lastSlash = input.lastIndexOf('/');
const pattern = input.slice(1,lastSlash);
const flags = input.slice(lastSlash+1);
try{ return new RegExp(pattern, flags); } catch(e){ throw e; }
}
// それ以外は生文字列。もし末尾に /g などが書かれていれば取り除く簡易対応
const slashFlagsMatch = input.match(/\\/(g|i|m|s|u|y)+$/);
if(slashFlagsMatch){
const flags = slashFlagsMatch[1];
const pattern = input.slice(0, input.length - (flags.length+1));
try{ return new RegExp(pattern, flags); } catch(e){ throw e; }
}
try{ return new RegExp(input); }catch(e){ throw e; }
}
// 採点ロジック
function grade(){
const exEls = document.querySelectorAll('.exercise');
let total=0, maxTotal=0;
exEls.forEach(el=>{
const id = Number(el.dataset.id);
const patInput = el.querySelector('.pattern').value.trim();
const resArea = el.querySelector('.results');
resArea.innerHTML = '';
const prob = problems[id];
maxTotal += (prob && prob.answer? (id===5?35: (id===4?20:(id===3?20:(id===2?15:10)))) : 0);
if(!patInput){
resArea.innerHTML = '<div class="ng">入力が空です</div>';
return;
}
// 問題別評価
try{
const userRe = parseRegex(patInput);
if(id===1 || id===2){
// 単純 true/false 判定テスト
const tests = prob.tests;
let okCount = 0;
const details = [];
tests.forEach((t,i)=>{
const expected = prob.expected[i];
const got = !!userRe.test(t);
// reset lastIndex for global regexes
if(userRe.global) userRe.lastIndex=0;
details.push(`『${t}』 → 期待: ${expected} / あなた: ${got}`);
if(got===expected) okCount++;
});
const score = Math.round((okCount/tests.length) * (id===1?10:(id===2?15:0)));
total += score;
resArea.innerHTML = `<div class="ok">結果: ${okCount}/${tests.length} 正解 — 得点 ${score} 点</div><pre>${details.join('\n')}</pre>`;
} else if(id===3){
// キャプチャ判定 — exec して groups をチェック
const tests = prob.tests;
let okCount = 0;
const details = [];
tests.forEach((t,i)=>{
const m = userRe.exec(t);
if(userRe.global) userRe.lastIndex=0;
const matchOK = !!m && m[1] && m[2] && m[3] && m[0]===t;
details.push(`『${t}』 → マッチ: ${!!m} / 年:${m?m[1]:"-"} 月:${m?m[2]:"-"} 日:${m?m[3]:"-"}`);
if(matchOK) okCount++;
});
const score = Math.round((okCount/tests.length) * 20);
total += score;
resArea.innerHTML = `<div class="ok">結果: ${okCount}/${tests.length} 正解(キャプチャで年/月/日を取得) — 得点 ${score} 点</div><pre>${details.join('\n')}</pre>`;
} else if(id===4){
// 置換判定
const repl = el.querySelector('.replacement').value;
if(typeof repl === 'undefined'){
resArea.innerHTML = '<div class="ng">置換文字列を入力してください</div>';
return;
}
const tests = prob.tests;
let okCount = 0;
const details = [];
tests.forEach((t,i)=>{
const got = t.replace(userRe, repl);
details.push(`『${t}』 → あなた: 『${got}』 / 期待: 『${prob.expected[i]}』`);
if(got===prob.expected[i]) okCount++;
if(userRe.global) userRe.lastIndex=0;
});
const score = Math.round((okCount/tests.length) * 20);
total += score;
resArea.innerHTML = `<div class="ok">結果: ${okCount}/${tests.length} 正解 — 得点 ${score} 点</div><pre>${details.join('\n')}</pre>`;
} else if(id===5){
// 抽出判定(match の配列比較)
const tests = prob.tests;
let okCount = 0;
const details = [];
tests.forEach((t,i)=>{
const matches = t.match(userRe) || [];
// JS の match は global フラグないと挙動が違うため、比較用に配列固定
const expected = prob.expected[i];
const same = JSON.stringify(matches)===JSON.stringify(expected);
details.push(`『${t}』 → あなた: ${JSON.stringify(matches)} / 期待: ${JSON.stringify(expected)}`);
if(same) okCount++;
if(userRe.global) userRe.lastIndex=0;
});
const score = Math.round((okCount/tests.length) * 35);
total += score;
resArea.innerHTML = `<div class="ok">結果: ${okCount}/${tests.length} 正解 — 得点 ${score} 点</div><pre>${details.join('\n')}</pre>`;
}
}catch(err){
resArea.innerHTML = `<div class="ng">正規表現の構文エラー: ${err.message}</div>`;
}
});
const scoreEl = document.getElementById('scoreArea');
scoreEl.innerHTML = `<strong>合計得点: ${total} / ${maxTotal}</strong>`;
}
document.getElementById('grade').addEventListener('click', grade);
document.getElementById('reset').addEventListener('click', ()=>{
document.querySelectorAll('.pattern').forEach(i=>i.value='');
document.querySelectorAll('.replacement').forEach(i=>i.value='');
document.querySelectorAll('.results').forEach(d=>d.innerHTML='');
document.getElementById('scoreArea').innerHTML='';
});
document.getElementById('showSolutions').addEventListener('click', ()=>{
// 各問題の模範解答を表示
document.querySelectorAll('.exercise').forEach(el=>{
const id = Number(el.dataset.id);
const res = el.querySelector('.results');
const prob = problems[id];
if(id===4){
res.innerHTML = `<div>模範: /${prob.answer}/g 置換: '${prob.replacement || ' '}'</div>`;
} else if(id===5){
res.innerHTML = `<div>模範: /${prob.answer}/${prob.flags||'g'}</div>`;
} else {
res.innerHTML = `<div>模範: /${prob.answer}/</div>`;
}
});
});
// 初回ヒント(サンプルを入れるにはここを編集)
// document.querySelector('.exercise[data-id="1"] .pattern').value = '^\\d+$';
</script>
</body>
</html>
HTMLできること(概要)
- 問題は5問(数字判定、メール、日付キャプチャ、置換、先頭大文字ワード抽出)。
- 正規表現は
/.../flags形式でも生文字列でも入力可能。 - 採点ボタンで自動採点し、各テストケースの詳細な結果と合計得点を表示。
- 「模範解答を表示」ボタンで例の正規表現を確認可能。
- 構文エラーはわかりやすく表示します。
