Open2

メモ

MKMK
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>固定長テキスト → CSV変換ツール</title>
  <style>
    :root{--bg:#0b0c0f;--panel:#151821;--muted:#7c8499;--text:#e6eaf2;--acc:#5aa9ff;--warn:#ffb454;--err:#ff5d5d;--ok:#6ee7b7}
    html,body{height:100%}
    body{margin:0;background:linear-gradient(180deg,#0b0c0f,#0f1220);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,"Hiragino Kaku Gothic ProN","Yu Gothic UI",Meiryo,sans-serif}
    .wrap{max-width:1200px;margin:40px auto;padding:0 16px}
    h1{font-size:22px;margin:0 0 12px}
    .sub{color:var(--muted);font-size:13px;margin-bottom:16px}
    .grid{display:grid;gap:16px}
    @media(min-width:980px){.grid{grid-template-columns:1.2fr .8fr}}
    .card{background:var(--panel);border:1px solid #22283a;border-radius:16px;box-shadow:0 10px 30px rgba(0,0,0,.25)}
    .card h2{margin:0;padding:14px 16px;border-bottom:1px solid #20263a;font-size:16px}
    .card .body{padding:14px 16px}
    textarea{width:100%;min-height:200px;resize:vertical;background:#0f1322;color:var(--text);border:1px solid #28314c;border-radius:12px;padding:10px;line-height:1.5}
    textarea::placeholder{color:#667099}
    .row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
    .row> *{margin:6px 0}
    select, input[type="text"], input[type="number"]{background:#0f1322;color:var(--text);border:1px solid #28314c;border-radius:10px;padding:8px 10px}
    .btn{appearance:none;border:1px solid #2a3558;background:#1b2340;color:#e6eaf2;padding:10px 14px;border-radius:12px;cursor:pointer;transition:all .15s ease;font-weight:600}
    .btn:hover{transform:translateY(-1px);box-shadow:0 6px 16px rgba(0,0,0,.25)}
    .btn.primary{background:linear-gradient(180deg,#2563eb,#1d4ed8);border-color:#1e40af}
    .btn.ghost{background:transparent}
    .tag{display:inline-flex;align-items:center;gap:6px;background:#0f1322;border:1px solid #28314c;color:#b9c2df;padding:6px 10px;border-radius:999px;font-size:12px}
    .note{font-size:12px;color:var(--muted)}
    .error{color:var(--err)}
    .ok{color:var(--ok)}
    table{width:100%;border-collapse:separate;border-spacing:0;overflow:auto}
    thead tr:nth-child(1) th{position:sticky;top:0;background:#11162a;z-index:2}
    thead tr:nth-child(2) th{position:sticky;top:36px;background:#11162a;z-index:2}
    thead tr:nth-child(3) th{position:sticky;top:72px;background:#11162a;z-index:2}
    th,td{border-bottom:1px solid #24304e;padding:8px 10px;font-size:13px;text-align:left}
    thead th{font-weight:700}
    tbody tr:nth-child(odd){background:#111527}
    .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}
    .toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:10px 0}
    .switch{display:inline-flex;align-items:center;gap:8px;cursor:pointer}
    .switch input{accent-color:#4f7cff}
    .footer{margin:18px 0;color:var(--muted);font-size:12px}
    .help{font-size:12px;color:#b9c2df}
    .kpd{display:inline-block;min-width:2ch;text-align:right}
  </style>
</head>
<body>
  <div class="wrap">
    <h1>固定長テキスト → CSV 変換ツール</h1>
    <div class="sub">項目定義(TSV)と固定長テキストを貼り付け、エンコーディング/可視化設定を選んで「変換」してください。</div>

    <div class="grid">
      <section class="card">
        <h2>① 項目定義(TSV)</h2>
        <div class="body">
          <div class="row note">形式: <span class="tag">name\tbytes</span> を行単位(例: <span class="mono">id\t4</span></div>
          <textarea id="schema" placeholder="id	4
name	5
area	12"></textarea>
          <div class="row">
            <label class="switch"><input id="hasHeader" type="checkbox"/> 1行目をヘッダとして扱う(データ側)</label>
          </div>
        </div>
      </section>

      <section class="card">
        <h2>② 固定長テキスト</h2>
        <div class="body">
          <textarea id="fixed" class="mono" placeholder="0001Yamada    Tokyo──────
0002Suzuki    Osaka──────"></textarea>
          <div class="row">
            <label class="switch"><input id="visualizeSpaces" type="checkbox" checked/> 表示上スペースを可視化(半角=· / 全角=□)</label>
          </div>
          <div class="row">
            <label>エンコーディング
              <select id="encoding">
                <option value="utf8" selected>UTF-8(厳密)</option>
                <option value="sjis">Shift_JIS 近似(ASCII/半角カナ=1B, それ以外=2B)</option>
              </select>
            </label>
            <label>行末
              <select id="linebreak">
                <option value="auto" selected>自動認識</option>
                <option value="\n">LF (\\n)</option>
                <option value="\r\n">CRLF (\\r\\n)</option>
              </select>
            </label>
            <button id="run" class="btn primary">変換</button>
            <button id="downloadCsv" class="btn">CSVダウンロード</button>
            <span id="status" class="note"></span>
          </div>
        </div>
      </section>
    </div>

    <section class="card" style="margin-top:16px">
      <h2>③ 結果</h2>
      <div class="body" id="result">
        <div class="help">ここにテーブルが表示されます。先頭3行は「項目名」「Byte数」「開始位置(1始まり)」です。</div>
      </div>
    </section>

    <div class="footer">
      注意: Shift_JIS はブラウザ標準APIに TextEncoder が無いため、一般的な近似ルールでバイト数を算出しています(ASCII/半角カナ=1バイト、それ以外=2バイト)。厳密なCP932変換が必要な場合は専用ライブラリの組み込みをご検討ください。
    </div>
  </div>

  <script>
    // ===== Utility: byte length & safe slice =====
    const encoders = {
      utf8Length: (s) => new TextEncoder().encode(s).length,
      utf8SliceByBytes: (s, maxBytes) => {
        const te = new TextEncoder();
        let bytes = 0, out = '';
        for (const ch of s){
          const bl = te.encode(ch).length;
          if (bytes + bl > maxBytes) break;
          bytes += bl; out += ch;
        }
        return {text: out, usedBytes: bytes};
      },
      // SJIS approximation: ASCII (<=0x7F) =1, 半角カナ U+FF61-FF9F =1, その他=2
      sjisLenOfChar: (ch) => {
        const code = ch.codePointAt(0);
        if (code <= 0x7F) return 1; // ASCII
        if (code >= 0xFF61 && code <= 0xFF9F) return 1; // 半角カナ
        return 2; // おおむね2B
      },
      sjisLength: (s) => {
        let n=0; for (const ch of s) n += encoders.sjisLenOfChar(ch); return n;
      },
      sjisSliceByBytes: (s, maxBytes) => {
        let bytes = 0, out='';
        for (const ch of s){
          const bl = encoders.sjisLenOfChar(ch);
          if (bytes + bl > maxBytes) break;
          bytes += bl; out += ch;
        }
        return {text: out, usedBytes: bytes};
      }
    };

    // Visible marks for spaces (display-only)
    const visualize = (s) => s.replaceAll(' ', '·').replaceAll('\u3000','□');

    // Parse schema TSV lines → [{name, bytes}]
    function parseSchema(tsv){
      const lines = tsv.split(/\r?\n/).map(l=>l.trim()).filter(Boolean);
      const schema = [];
      for (let i=0;i<lines.length;i++){
        const [name, bytesStr] = lines[i].split(/\t+/);
        if (!name || !bytesStr) throw new Error(`項目定義 ${i+1}行目の形式が不正です(name\tbytes)`);
        const bytes = Number(bytesStr);
        if (!Number.isInteger(bytes) || bytes<=0) throw new Error(`項目定義 ${i+1}行目のByte数が不正です: ${bytesStr}`);
        schema.push({name, bytes});
      }
      return schema;
    }

    function splitLines(text, mode){
      if (mode === '\\n') return text.split('\n');
      if (mode === '\\r\\n') return text.split('\r\n');
      // auto
      if (text.includes('\r\n')) return text.split('\r\n');
      return text.split('\n');
    }

    // Fixed-width split by bytes
    function splitFixedBySchema(line, schema, encoding){
      const chunks = [];
      let rest = line;
      let encLen, encSlice;
      if (encoding==='utf8'){
        encLen = encoders.utf8Length; encSlice = encoders.utf8SliceByBytes;
      } else {
        encLen = encoders.sjisLength; encSlice = encoders.sjisSliceByBytes;
      }
      for (const field of schema){
        const {text: part, usedBytes} = encSlice(rest, field.bytes);
        chunks.push(part);
        // remove consumed chars from rest
        rest = rest.slice(part.length);
        // If consumed bytes < field.bytes but no more chars, pad empty remainder (display only)
      }
      // Remaining chars ignored
      return chunks;
    }

    // Build table DOM
    function renderTable(container, schema, records, opts){
      const enc = opts.encoding;
      // Compute start positions (1-based)
      const starts = [];
      let pos = 1;
      for (const f of schema){ starts.push(pos); pos += f.bytes; }

      const table = document.createElement('table');
      const thead = document.createElement('thead');
      const tbody = document.createElement('tbody');

      const tr1 = document.createElement('tr');
      for (const f of schema){ const th = document.createElement('th'); th.textContent = f.name; tr1.appendChild(th);} 

      const tr2 = document.createElement('tr');
      for (const f of schema){ const th = document.createElement('th'); th.textContent = `${f.bytes}`; tr2.appendChild(th);} 

      const tr3 = document.createElement('tr');
      for (const s of starts){ const th = document.createElement('th'); th.textContent = `${s}`; tr3.appendChild(th);} 

      thead.appendChild(tr1); thead.appendChild(tr2); thead.appendChild(tr3);

      for (const rec of records){
        const tr = document.createElement('tr');
        rec.forEach(val => {
          const td = document.createElement('td');
          td.className = 'mono';
          td.textContent = opts.visualize ? visualize(val) : val;
          tr.appendChild(td);
        });
        tbody.appendChild(tr);
      }
      table.appendChild(thead); table.appendChild(tbody);
      container.innerHTML = '';
      container.appendChild(table);
    }

    function toCSV(rows, visualizeSpaces){
      // RFC4180-ish, escaping quotes
      const esc = (s) => {
        const t = visualizeSpaces ? visualize(s) : s;
        if (t == null) return '';
        const needQuote = /[",\n\r]/.test(t) || t.includes(',');
        const x = String(t).replaceAll('"','""');
        return needQuote ? `"${x}"` : x;
      };
      return rows.map(r => r.map(esc).join(',')).join('\r\n');
    }

    function parse(){
      const status = document.getElementById('status');
      status.textContent = '';
      try {
        const schema = parseSchema(document.getElementById('schema').value);
        const fixedRaw = document.getElementById('fixed').value.replace(/\u0000/g,'');
        const linebreak = document.getElementById('linebreak').value;
        const lines = splitLines(fixedRaw, linebreak).filter(l=>l.length>0);
        const encoding = document.getElementById('encoding').value;
        const visualizeSpaces = document.getElementById('visualizeSpaces').checked;

        const records = [];
        for (let i=0;i<lines.length;i++){
          const row = splitFixedBySchema(lines[i], schema, encoding);
          records.push(row);
        }

        // Header rows for CSV
        const headerNames = schema.map(f=>f.name);
        const headerBytes = schema.map(f=>String(f.bytes));
        const headerStarts = (()=>{let a=[];let p=1;for(const f of schema){a.push(String(p));p+=f.bytes;}return a;})();

        renderTable(document.getElementById('result'), schema, records, {visualize: visualizeSpaces, encoding});

        // cache for download
        window.__csvRows = [headerNames, headerBytes, headerStarts, ...records];
        status.innerHTML = `<span class="ok">変換完了:</span> 行数 <b>${records.length}</b>`;
      } catch (e){
        document.getElementById('result').innerHTML = '';
        status.innerHTML = `<span class="error">エラー:</span> ${e.message}`;
        console.error(e);
      }
    }

    function download(){
      const rows = window.__csvRows || [];
      if (!rows.length){
        document.getElementById('status').innerHTML = `<span class="error">先に「変換」を実行してください。</span>`;
        return;
      }
      const visualizeSpaces = document.getElementById('visualizeSpaces').checked;
      const csv = toCSV(rows, visualizeSpaces);
      const blob = new Blob([csv], {type:'text/csv;charset=utf-8'});
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url; a.download = 'fixed_to_csv.csv';
      document.body.appendChild(a); a.click(); a.remove();
      URL.revokeObjectURL(url);
    }

    document.getElementById('run').addEventListener('click', parse);
    document.getElementById('downloadCsv').addEventListener('click', download);

    // Demo preload
    document.getElementById('schema').value = 'id\t4\nname\t5\narea\t12';
    document.getElementById('fixed').value = '0001Yamad\u0020Tokyo\u2500\u2500\u2500\u2500\u2500\u2500\n0002Suzuk\u0020Osaka\u2500\u2500\u2500\u2500\u2500\u2500';
  </script>
</body>
</html>
MKMK
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>固定長テキスト → 表示(SJIS想定)</title>
  <style>
    body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"Hiragino Kaku Gothic ProN","Yu Gothic UI",Meiryo,sans-serif;margin:16px}
    h1{font-size:20px;margin:0 0 10px}
    h2{font-size:16px;margin:18px 0 8px}
    textarea{width:100%;min-height:180px;resize:vertical;padding:6px}
    .row{margin:8px 0}
    button{padding:6px 12px}
    table{border-collapse:collapse;width:100%;margin-top:10px}
    th,td{border:1px solid #999;padding:4px 6px;font-size:13px;text-align:left}
    th{background:#f5f5f5}
    .mono{font-family:ui-monospace,Consolas,Monaco,monospace}
    .note{font-size:12px;color:#444}
    .error{color:#c00}
    .ok{color:#060}
  </style>
</head>
<body>
  <h1>固定長テキスト → テーブル(Shift_JIS想定)</h1>

  <h2>① 項目定義(TSV)</h2>
  <div class="note">形式: <code>name\tbytes</code> を行単位(例: <code>id\t4</code>)。</div>
  <textarea id="schema" placeholder="id\t4\nname\t5\narea\t12"></textarea>

  <h2>② 固定長テキスト(複数行)</h2>
  <textarea id="fixed" class="mono" placeholder="0001Yamada    Tokyo──────\n0002Suzuki    Osaka──────"></textarea>
  <div class="row">
    <label><input id="visualizeSpaces" type="checkbox" checked> スペース可視化(半角=· / 全角=□)</label>
  </div>
  <div class="row">
    <button id="run">変換</button>
    <span id="status" class="note"></span>
  </div>

  <h2>③ 結果</h2>
  <div id="result" class="note">ここにテーブルが表示されます。先頭3行は「項目名」「Byte数」「開始位置(1始まり)」です。</div>

  <script>
    // --- Shift_JIS(CP932相当)の簡易バイト長ルール ---
    // 近似: ASCII(0x00-0x7F)=1B、半角カナ(U+FF61-FF9F)=1B、その他=2B。
    // ※厳密変換が必要ならライブラリ置換が必要です。
    function sjisLenOfChar(ch){
      const code = ch.codePointAt(0);
      if (code <= 0x7F) return 1;                       // ASCII
      if (code >= 0xFF61 && code <= 0xFF9F) return 1;   // 半角カナ
      return 2;                                         // その他(全角類)
    }
    function sjisSliceByBytes(str, maxBytes){
      let out = '', used = 0;
      for (const ch of str){
        const bl = sjisLenOfChar(ch);
        if (used + bl > maxBytes) break;
        out += ch; used += bl;
      }
      return {text: out, usedBytes: used};
    }

    // 表示用のスペース可視化
    const visualize = (s) => s.replaceAll(' ', '·').replaceAll('\u3000','□');

    // TSVスキーマ解析
    function parseSchema(tsv){
      const lines = tsv.split(/\r?\n/).map(l=>l.trim()).filter(Boolean);
      const schema = [];
      for (let i=0;i<lines.length;i++){
        const [name, bytesStr] = lines[i].split(/\t+/);
        if (!name || !bytesStr) throw new Error(`${i+1}行目: 形式は name\tbytes です`);
        const bytes = Number(bytesStr);
        if (!Number.isInteger(bytes) || bytes<=0) throw new Error(`${i+1}行目: Byte数が不正です`);
        schema.push({name, bytes});
      }
      return schema;
    }

    // 行分割(CRLF/LF自動)
    function splitLines(text){
      if (text.includes('\r\n')) return text.split('\r\n');
      return text.split('\n');
    }

    // 固定長分割(SJIS前提)
    function splitFixedBySchema(line, schema){
      let rest = line; const chunks = [];
      for (const f of schema){
        const {text: part} = sjisSliceByBytes(rest, f.bytes);
        chunks.push(part);
        rest = rest.slice(part.length);
      }
      return chunks;
    }

    // テーブル描画(先頭3行: 項目名 / Byte数 / 開始位置)
    function renderTable(container, schema, records, visualizeSpaces){
      const starts = []; let pos = 1; for (const f of schema){ starts.push(pos); pos += f.bytes; }
      const table = document.createElement('table');
      const thead = document.createElement('thead');
      const tbody = document.createElement('tbody');

      const tr1 = document.createElement('tr');
      schema.forEach(f=>{const th=document.createElement('th');th.textContent=f.name;tr1.appendChild(th)});
      const tr2 = document.createElement('tr');
      schema.forEach(f=>{const th=document.createElement('th');th.textContent=f.bytes;tr2.appendChild(th)});
      const tr3 = document.createElement('tr');
      starts.forEach(s=>{const th=document.createElement('th');th.textContent=s;tr3.appendChild(th)});
      thead.appendChild(tr1); thead.appendChild(tr2); thead.appendChild(tr3);

      records.forEach(rec=>{
        const tr=document.createElement('tr');
        rec.forEach(val=>{
          const td=document.createElement('td');
          td.className='mono';
          td.textContent = visualizeSpaces ? visualize(val) : val;
          tr.appendChild(td);
        });
        tbody.appendChild(tr);
      });

      table.appendChild(thead); table.appendChild(tbody);
      container.innerHTML=''; container.appendChild(table);
    }

    function run(){
      const status = document.getElementById('status');
      status.textContent = '';
      try{
        const schema = parseSchema(document.getElementById('schema').value);
        const lines = splitLines(document.getElementById('fixed').value).filter(l=>l.length>0);
        const visualizeSpaces = document.getElementById('visualizeSpaces').checked;
        const records = lines.map(line => splitFixedBySchema(line, schema));
        renderTable(document.getElementById('result'), schema, records, visualizeSpaces);
        status.innerHTML = `<span class="ok">変換完了: 行数 ${records.length}</span>`;
      }catch(e){
        document.getElementById('result').innerHTML='';
        status.innerHTML = `<span class="error">エラー: ${e.message}</span>`;
      }
    }

    document.getElementById('run').addEventListener('click', run);

    // デモ用初期値
    document.getElementById('schema').value = 'id\t4\nname\t5\narea\t12';
    document.getElementById('fixed').value = '0001Yamad Tokyo──────\n0002Suzuk Osaka──────';
  </script>
</body>
</html>