0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

改行コード変換(CRLF/LF/CR)をブラウザで実装する — 正規化の置換順序の罠と、混在を数えるカウント

0
Posted at

3行まとめ

  • CRLF・LF・CR を相互変換するブラウザ完結ツールを作った。肝は「一度LFに正規化してから目的の改行コードへ変換する」2段構え
  • 正規化の置換は \r\n\n を先、\r\n を後にやる。順序を逆にすると \r\n\n\n になって空行が倍増する
  • 混在テキストのカウントは、先にCRLFを取り除いてから単独のLF・CRを数える。そうしないとCRLFの中の \n \r を二重計上する

WindowsのExcelでLF改行のCSVを開いたら全部1行になった、Linuxにアップしたシェルスクリプトが /bin/bash^M: bad interpreter で動かない、Gitで改行コードの差分が大量に出る——改行コードの違いは、OSをまたぐと地味に牙をむく。

ぱんだツールズの改行コード変換は、テキストの改行を CRLF(Windows)・LF(Linux/macOS)・CR(旧Mac)に相互変換するツール。入力に含まれる各改行コードの数もリアルタイムで表示する。処理はブラウザ内で完結し、テキストはサーバーに送信されない。

実装は正規表現の置換だけだが、「置換の順序」と「混在のカウント」に、素朴にやると間違える落とし穴がある。実装ベースで解説する。

3つの改行コード

まず前提。改行コードは歴史的に3種類ある。

表記 バイト 主なOS
CRLF \r\n(0x0D 0x0A・2バイト) Windows
LF \n(0x0A・1バイト) Linux / macOS(OS X以降)
CR \r(0x0D・1バイト) 旧 Mac OS(9以前)

CR(キャリッジリターン)は「行頭に戻る」、LF(ラインフィード)は「1行送る」の意味で、タイプライター由来。Windowsはこの2つを繋げた CRLF、Unix系は LF だけ、という分岐が今も残っている。

変換は「LFに正規化してから」の2段構え

CRLF ⇄ LF ⇄ CR を全組み合わせで個別に書くと分岐が増える。代わりに、まず何であれ一度LFに正規化し、そこから目的の改行コードへ変換する2段構えにすると、変換関数が1本で済む。

function convertNewlines(text: string, to: NewlineType): string {
  // ① まずすべての改行コードを LF に正規化
  const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
  // ② 目的の改行コードへ
  switch (to) {
    case 'CRLF': return normalized.replace(/\n/g, '\r\n')
    case 'LF':   return normalized
    case 'CR':   return normalized.replace(/\n/g, '\r')
  }
}

入力がCRLF混在でもCR単独でも、①でいったんLFに揃うので、②は「LFを目的コードに置換」の1手で済む。中間表現(LF)を1つ決めて、入力の多様さをそこへ潰す——という発想で、これは文字コードや日付の変換でもよく使う設計。

置換の順序が結果を変える

①の正規化に、順序の罠がある。\r\n\n先に\r\n後にやらないといけない。

text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')   // ✅ 正しい

もし順序を逆にして \r\n を先にやると、\r\n(CRLF)の中の \r が先に \n へ変わって \n\n になり、続く \r\n の置換は対象を失う。結果、CRLF1個が空行1つ分(\n\n)に化けて、行数が倍増する

// ❌ 逆順だと CRLF が壊れる
'a\r\nb'.replace(/\r/g, '\n').replace(/\r\n/g, '\n')
// → 'a\n\nb'(空行が1つ増える)

「CRLFはCRとLFの2文字」という事実がここで効いてくる。長いパターン(\r\n)を短いパターン(\r)より先に処理するのが、この手の正規化の鉄則。逆に言えば、CRLFを1つの単位として先に消化してしまえば、残った \r は本物の単独CRだけになる。

混在のカウントも「CRLFを先に除く」

このツールは入力に含まれる CRLF・LF・CR の個数を表示して、改行コードが混在しているかを見えるようにしている。ここでも同じ「CRLFを先に処理」の考え方が要る。

function countNewlines(text: string) {
  const crlf = (text.match(/\r\n/g) ?? []).length
  const lf = (text.replace(/\r\n/g, '').match(/\n/g) ?? []).length   // CRLFを除いてからLFを数える
  const cr = (text.replace(/\r\n/g, '').match(/\r/g) ?? []).length   // CRLFを除いてからCRを数える
  return { crlf, lf, cr }
}

素朴に text.match(/\n/g) でLFを数えると、CRLFの中に含まれる \n まで数えてしまい、「LFの数」がCRLFの分だけ水増しされる。CR も同様。だから 先に \r\n を取り除いてから、残った単独のLF・CRを数える。こうすると crlf / lf / cr の3つが「単独で出現した回数」として正しく分離され、たとえば「CRLFが10・LFが3」と出れば混在していると一目で分かる。

match は該当なしのとき null を返すので、?? [] で空配列に受けてから .length を取っているのも地味だが必要な処理(null.length は例外になる)。

なぜ改行コードの変換が要るのか

最後に実用面。改行コードの不一致は、こういう場面で問題になる。

  • ExcelでCSVが1行になる:LF改行のCSVをWindowsのExcelが改行と認識できず、全行が1行に潰れる。LF→CRLFで解決する(あるいはBOM付きUTF-8にする)
  • シェルスクリプトが動かない:Windowsで作ったCRLFのスクリプトをLinuxに置くと、行末の \r が余分な文字として解釈され、/bin/bash^M: bad interpreter などのエラーになる。CRLF→LFで直る
  • Gitの差分が大量に出る:Windows勢とMac/Linux勢が混在すると改行コードがOSごとにコミットされ差分が膨れる。.gitattributescore.autocrlf で統一するのが本筋だが、単発ならLFに変換してからコミットするだけでも収まる

現代のソースコードは LF に統一するのが一般的(GitHub/GitLab も LF が標準)。CSVをExcelに渡すような場面だけCRLFが要る、という住み分けを押さえておくと、どちらに変換すべきか迷わない。

まとめ

  • 改行コード変換は「一度LFに正規化 → 目的コードへ」の2段構えにすると変換関数が1本で済む。中間表現を1つ決めるのが定石
  • 正規化の置換は \r\n\n を先、\r\n を後に。逆順だとCRLFが \n\n に化けて行数が倍増する。長いパターンを先に処理する
  • 混在カウントは先にCRLFを除いてから単独LF・CRを数える。素朴に数えるとCRLF内の \n \r を二重計上する。matchnull?? [] で受ける
  • 用途はExcelのCSV(CRLF)・シェルスクリプトやGit(LF)。ソースは基本LF、Excel渡しだけCRLF

OS間でテキストをやり取りして改行が崩れたときにどうぞ。テキストはブラウザの外に出ない。

ぱんだツールズ では他にも 文字コード変換・CSV変換・全角半角変換・テキスト整形など、テキストの実務処理に効くブラウザ完結ツールを多数公開中。全部無料・登録不要・ファイルはサーバーに送られない。
https://sakutto-panda.com


この記事は Zenn にも同じ内容を投稿しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?