<!DOCTYPE html>
<html lang="ja-JP">
  <head>
    <meta charset="UTF-8" />
    <title>ライブコードサンプル</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/css2?family=Roboto&family=Noto+Sans+JP&family=Noto+Color+Emoji&display=swap"
    />
    <link rel="stylesheet" href="lib/style.css" />
  </head>

  <body>
    <h1>時間のかかる処理</h1>
    <p>日本の「新川」という地名のうち、郵便番号が素数のものを抜き出します!</p>
    <p>インターバル処理: <span id="random"></span></p>
    <button id="heavy">重い処理スタート</button>
    <section id="report"></section>
    <table id="table">
      <thead></thead>
      <tbody></tbody>
    </table>
    <script src="lib/script.js"></script>
  </body>
</html>
/** ボックスサイズを `border-box` にそろえる */
html {
  box-sizing: border-box;
}
*,
*::before,
*::after {
  box-sizing: inherit;
  font-style: normal;
}

/** サイトの土台を定義 */
html,
body {
  font-family: 'Roboto', 'Noto Sans JP', sans-serif;
  font-size: 16px;
  font-weight: normal;
  margin: 0;
  padding: 0;
}

#report {
  margin-top: 1rem;
  border: 1px solid #000;
}

#table {
  border: 1px solid #000;
  border-collapse: collapse;
}

#table th, #table td {
  border: 1px solid #000;
  padding: 0.2rem;
}
/** Worker */
const worker = new Worker('./lib/worker.js');

// サブスレッドからのメッセージを待ち受けるイベントを定義
worker.addEventListener('message', onMessage);

const random = document.querySelector('#random');
const btnBegin = document.querySelector('#heavy');
const report = document.querySelector('#report');
const table = document.querySelector('#table');

// ボタンが押されたときのイベントを定義
btnBegin.addEventListener('click', main);

setInterval(()=>random.textContent = Math.floor(Math.random()*999), 100)

let start = 0;
let head = '';
/**
 * メイン処理
 *
 * @returns {Promise<string>}
 */
async function main() {
  btnBegin.removeEventListener('click', main);
  clear();

  report.append(makeParagraph('CSVを読み込む'));
  const csv = await readCSV();
  await splitMacroTask();

  report.append(makeParagraph('CSVをパースする'));
  await splitMacroTask();
  const body = csv.split('\n');
  head = body.shift();
  const locations = parseCSV(body);

  report.append(makeParagraph('郵便番号が素数である項目を抜き出す'));
  await splitMacroTask();

  start = Date.now();
  /** 素数を抜き出す処理 */
  worker.postMessage(locations);
}

/**
 * サブスレッドからのメッセージ受信時の処理
 * 
 * @param {MessageEvent} e メッセージイベント
 * @param {string[][]} e.data イベントに添えられたメッセージ
 */
async function onMessage(e) {
  const result = e.data;

  report.append(makeParagraph(`件数: ${result.length} 件`));
  await splitMacroTask();

  report.append(
    makeParagraph(`重い処理にかかった時間: ${Date.now() - start} ミリ秒`)
  );

  showResult(head.split(','), result);

  btnBegin.addEventListener('click', main);
};

/**
 * タスク分割用(描画のヘルパです)
 */
const splitMacroTask = () => {
  return new Promise((res) => setTimeout(res, 0));
};

/**
 * クリア用
 */
const clear = () => {
  report.replaceChildren();
  table.querySelector('thead').replaceChildren();
  table.querySelector('tbody').replaceChildren();
};
/**
 * 文字のある <p> エレメントを得る
 *
 * @param {string[]} strArr
 * @returns {string[][]}
 */
const makeParagraph = (str) => {
  const p = document.createElement('p');
  p.textContent = str;

  return p;
};
/**
 * 結果を <table> として表示する
 *
 * @param {string[]} head
 * @param {string[][]} result
 */
const showResult = (head, result) => {
  const trHead = getTRNode(head, true);
  table.querySelector('thead').append(trHead);
  for (const row of result) {
    const trBody = getTRNode(row);
    table.querySelector('tbody').append(trBody);
  }
};
/**
 * 結果の <tr> タグを得る
 *
 * @param {string[]} indexies
 * @param {boolean} [isHead=false]
 */
const getTRNode = (indexies, isHead = false) => {
  const [, , zip, , area, district] = indexies;
  const tr = document.createElement('tr');
  const cell = isHead ? 'th' : 'td';
  tr.innerHTML = `<${cell}>${zip}</${cell}><${cell}>${area}</${cell}><${cell}>${district}</${cell}>`;

  return tr;
};

/**
 * CSVを読み込む
 *
 * @returns {Promise<string>}
 */
const readCSV = async () => {
  const response = await fetch(
    'https://postcode.teraren.com/postcodes.csv?s=新川'
  );
  const parsed = await response.arrayBuffer();
  const decoder = new TextDecoder('shift-jis');

  return decoder.decode(parsed).trim();
};

/**
 * CSVをノーマライズして配列化
 *
 * @param {string[]} strArr
 * @returns {string[][]}
 */
const parseCSV = (strArr) =>
  strArr.map((str) => str.replace('""', '').split(','));
/**
 * メッセージ受信時の処理
 * 
 * @param {MessageEvent} e メッセージイベント
 * @param {string[][]} e.data イベントに添えられたメッセージ
 */
self.onmessage = (e) => {
  const locations = e.data;
  self.postMessage(filterPrime(locations));
};
/**
 * 郵便番号の合計が素数である項目を抜き出す。ここでは「試し割り法」を使用。
 *
 * @param {string[][]} body
 * @returns {string[][]}
 */
const filterPrime = (body) => body.filter((row) => trialDivision(+row[2]));

/**
 * 「試し割り法」で素数かどうかを検証
 *
 * @param {number} n 対象の数
 * @returns {boolean} 素数かどうか
 */
const trialDivision = (n) => {
  if (n < 2) {
    return false;
  }
  // 1 づつ数値を上げて、割り切れるかどうかを検証
  for (let i = 2; i < n; i++) {
    // 割り切れるか判定
    if (n % i == 0) {
      // 剰余がなければ割り切れているので、素数ではない
      return false;
    }
  }

  // 最後まで割り切れなければ、それは素数である
  return true;
};