JavaScript | 第9章「正規表現」

javascrpit JavaScript
スポンサーリンク

練習問題+自動採点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 形式でも生文字列でも入力可能。
  • 採点ボタンで自動採点し、各テストケースの詳細な結果と合計得点を表示。
  • 「模範解答を表示」ボタンで例の正規表現を確認可能。
  • 構文エラーはわかりやすく表示します。
タイトルとURLをコピーしました