LoginSignup
79
50

【JavaScript】紆余曲折の末、『最も直感的に文字列に改行を入れる方法』にたどり着いた話

Last updated at Posted at 2023-01-22

結論

Array.prototype.join() を使う

Array.prototype.join() を使うとスッキリ書くことができる。
以下のように書くと、生成される文字列もコードの見た目通りに改行される。
こちらの想定していない空白や改行は一切入らない。

const text = [
  'JavaScript (JS) は軽量で、',
  'インタープリター型、あるいは実行時コンパイルされる、',
  '第一級関数を備えたプログラミング言語です。',
].join('\n');
console.log(text);

JavaScript (JS) は軽量で、
インタープリター型、あるいは実行時コンパイルされる、
第一級関数を備えたプログラミング言語です。

変数の埋め込みもできる

もちろんテンプレートリテラルを使った変数の埋め込みもできる。

const lang = 'JavaScript(JS)';
const text = [
  `${lang} は軽量で、`,
  'インタープリター型、あるいは実行時コンパイルされる、',
  '第一級関数を備えたプログラミング言語です。',
].join('\n');
console.log(text);

JavaScript (JS) は軽量で、
インタープリター型、あるいは実行時コンパイルされる、
第一級関数を備えたプログラミング言語です。

空の改行も簡単に入れられる

空白の改行を入れたいときは空の文字列入れるだけ。

const text = [
  'JavaScript には変数を宣言する方法が 3 種類あります。',
  '',
  'var',
  ' 変数を宣言し、ある値に初期化することもできる。',
  '',
  'let',
  ' ブロックスコープのローカル変数を宣言し、ある値に初期化することもできる。',
  '',
  'const',
  ' ブロックスコープで読み取り専用の名前付き定数を宣言する。',
].join('\n');
console.log(text);

JavaScript には変数を宣言する方法が 3 種類あります。

var
 変数を宣言し、ある値に初期化することもできる。

let
 ブロックスコープのローカル変数を宣言し、ある値に初期化することもできる。

const
 ブロックスコープで読み取り専用の名前付き定数を宣言する。

紆余曲折

① ど直球に改行を入れていた時代

一番最初は面倒だと思いつつ工夫の仕方がわからなかったため
ど直球にこんな風に書いていました。

const text = 
'JavaScript (JS) は軽量で、' + '\n' +
'インタープリター型、あるいは実行時コンパイルされる、' + '\n' +
'第一級関数を備えたプログラミング言語です。';
console.log(text);

JavaScript (JS) は軽量で、
インタープリター型、あるいは実行時コンパイルされる、
第一級関数を備えたプログラミング言語です。

改行の数だけ毎回 + '\n' + を書かないといけないのでとても面倒です。

またコードの見た目と出力結果を見比べると、
ちょっと末尾に毎回ついている + '\n' + が邪魔に見えて
なんだか直感的ではない印象です。

② テンプレートリテラルを使おうとした時代

前述のど直球な文字列結合しか知らなかった自分が
テンプレートリテラルをはじめて知ったときは
「こんな便利なものがあったなんて...!」
と感動したことを覚えています。

が、改行を取り扱おうとするとちょっと使いづらいです。

const text = `
JavaScript (JS) は軽量で、
インタープリター型、あるいは実行時コンパイルされる、
第一級関数を備えたプログラミング言語です。
`;
console.log(text);

 
JavaScript (JS) は軽量で、
インタープリター型、あるいは実行時コンパイルされる、
第一級関数を備えたプログラミング言語です。
 

例えばこのように書くと何の工夫もなく改行を入れてくれて便利なのですが、
文章の前後にも改行が入ってしまいます。
(少し出力例が分かりにくいですが...)

コードの見た目を考えて入れている改行まで
ご丁寧に汲み取ってくれてしまうからです。

--

これは以下のように書けば解決します。

const text = `JavaScript (JS) は軽量で、
インタープリター型、あるいは実行時コンパイルされる、
第一級関数を備えたプログラミング言語です。`;
console.log(text);

JavaScript (JS) は軽量で、
インタープリター型、あるいは実行時コンパイルされる、
第一級関数を備えたプログラミング言語です。

が、コードの見た目と実際に出力される結果に差がありすぎるため
個人的には好きになれませんでした...

③ そして今

Array.prototype.join() を使うところに落ち着いています。

この方法が自分が知っている中で一番
・コードの見た目
・実際に出力される文字列
に差がないと感じるためです。

const text = [
  'JavaScript (JS) は軽量で、',
  'インタープリター型、あるいは実行時コンパイルされる、',
  '第一級関数を備えたプログラミング言語です。',
].join('\n');
console.log(text);

JavaScript (JS) は軽量で、
インタープリター型、あるいは実行時コンパイルされる、
第一級関数を備えたプログラミング言語です。

④ これから...?

しばらくは Array.prototype.join() を使うので落ち着きそうかなと思っていますが、
まだ自分が知らないだけでもっと直感的で簡単な方法があるかもしれません。

もし「自分はこうやってるぞ!」という知見をお持ちの方がいらっしゃいましたら
ご教示いただけると嬉しいです...!🙏

参考

Array.prototype.join() - JavaScript - MDN Web Docs - Mozilla
テンプレートリテラル (テンプレート文字列) - JavaScript | MDN

追記

2023/01/22 : String.prototype.trim() を使う

@ss8826 さんよりコメントにて
String.prototype.trim() を使うとテンプレートリテラル使用時にできてしまう
不用意な空白を消すことができるとご指摘いただきました。ありがとうございます!🙏

const text = `
JavaScript (JS) は軽量で、
インタープリター型、あるいは実行時コンパイルされる、
第一級関数を備えたプログラミング言語です。
`.trim();
console.log(text);

JavaScript (JS) は軽量で、
インタープリター型、あるいは実行時コンパイルされる、
第一級関数を備えたプログラミング言語です。

狙い通り、 trim() によって文章前後に生じた改行を消すことができています。

--

ただ、trim() が削除してくれるのは『文字列の両端の空白』だけなので、
次の例のように『インデントによって文字列中に発生する空白』については対応ができません。

// if文によってインデントがずれる場合
if (true) {
  const text = `
  JavaScript (JS) は軽量で、
  インタープリター型、あるいは実行時コンパイルされる、
  第一級関数を備えたプログラミング言語です。
  `.trim();
  console.log(text);
}

JavaScript (JS) は軽量で、
 インタープリター型、あるいは実行時コンパイルされる、
 第一級関数を備えたプログラミング言語です。

テンプレートリテラルはインデントまで拾ってくれてしまうので、
コードの見た目を整えようとすると
出力される文字列にまで影響が及んでしまうのが難しいですね...

2023/01/25 : 正規表現を使って行頭の空白文字数を数え上げる

@jun1s さんよりコメントにてご教示いただいた方法です。
ありがとうございます!

// 行頭のインデントを全行から削除する関数
const trimIndent = (str) => {
  const lines = str?.split(/\n/); // 文字列を改行で区切って配列 lines に格納
  const firstLine = lines?.find((line) => line); // 1行目を取得 → 変数 firstLine に格納
  const countIndent = (line) => line?.match(/^\s+/)?.[0].length ?? 0; // 行頭の空白文字数を数える関数を定義
  const indent = countIndent(firstLine); // 行頭の空白文字数を数える → 変数 indent に格納
  const regexStr = `^\\s{${indent}}`; // 行頭の空白文字に一致する正規表現を定義

  // 全行の行頭空白文字を削除 → 改行で結合 → 文字列前後の空白削除
  return lines
    ?.map((line) => line.replace(new RegExp(regexStr), ""))
    .join("\n")
    .trim();
};

この関数を使うと、行頭の空白文字を全て削除してくれるため
インデントによってテンプレート構文内に発生する無駄な空白も怖くなくなります。

if (true) {
  const text = `
  JavaScript (JS) は軽量で、
  インタープリター型、あるいは実行時コンパイルされる、
  第一級関数を備えたプログラミング言語です。
  `;
  console.log(trimIndent(text));
}

JavaScript (JS) は軽量で、
インタープリター型、あるいは実行時コンパイルされる、
第一級関数を備えたプログラミング言語です。

2023/01/25 : String.dedent なるものが使えるようになるかも...?

@mizozobu さんよりコメントにてご教示いただいた内容です。

String.dedent という関数の実装が検討されているようです。
tc39/proposal-string-dedent

class MyClass {
  print() {
    console.log(String.dedent`
      create table student(
        id int primary key,
        name text
      )
    `);
  }
}

create table student(
 id int primary key,
 name text
)

まさに自分が求めていたコードの見た目、出力結果そのものです。
もう今から楽しみです。

--

蛇足になりますが、Problem に書かれている問題意識が
自分が感じていたそれと全く同じだったので
「同じように感じている人は自分だけじゃないんだ...」
と安堵に似た感情を抱きました。笑

以下、Problem の内容一部抜粋 & 日本語訳

JSコードにフォーマットされたテキスト(例えばMarkdownのコンテンツやJSプログラムのソーステキスト)を埋め込もうとすると、開発者はコードや出力の可読性のために厄介な譲歩をすることを余儀なくされます。
例えば、埋め込みテキストを周囲のコードと馴染むように書くと次のようになります。

class MyClass {
  print() {
    console.log(`
      create table student(
        id int primary key,
        name text
      )
    `);
  }
}

この出力は以下のようになります。(^で行頭を、-で先頭の空白を表す)

^
^······create table student(
^········id int primary key,
^········name text
^······)
^····

出力を直感的に見やすくしようとすると、今度はコードが読みづらくなります。

class MyClass {
  print() {
    console.log(`create table student(
  id int primary key,
  name text
)`);
  }
}

この出力は以下のように直感的に見やすくなります。

create table student(
  id int primary key,
  name text
)

詳細に関しては、リンク先をご覧ください。
tc39/proposal-string-dedent

79
50
12

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
79
50