🤝

単語ごとに分割された文章を日本語は空文字で/英語はスペースでjoinする

2023/06/02に公開

以下のように単語ごとに分割された文章を連結したいことがあります。

const arr = [
  ["わたし", "は", "ホットドッグ", "が", "とても", "好き", "です", "。"],
  ["わたし", "は", "hotto", "doggu", "が", "とても", "好き", "です", "。"],
  ["I", "like", "hotto", "doggu", "very", "much", "."],
];

しかし、上記のように日本語文と英語文のどちらが渡されるかわからない(あるいは途中で混じっている)場合、何でjoinするかが問題となります。
日本語文に基本的に空白は入りませんが、アルファベットではスペースで単語を分けて書くためです。

arr.forEach((x) => console.log(x.join("")));
// わたしはホットドッグがとても好きです。
// わたしはhottodogguがとても好きです。
// Ilikehottodogguverymuch.

arr.forEach((x) => console.log(x.join(" ")));
// わたし は ホットドッグ が とても 好き です 。
// わたし は hotto doggu が とても 好き です 。
// I like hotto doggu very much .

この問題は、先に適当な文字列でjoinし、それをreplaceすることで解決できます。
以下に実装例を示します。joinTokenは普通の文中に出てこない文字列であれば何でも良いと思います。

const smartJoin = (words: string[]) => {
  const joinToken = "__JOIN_TOKEN__";
  const joinAlnum = new RegExp(`(\\w)${joinToken}(\\w)`, "g");

  return words.join(joinToken)
    .replace(joinAlnum, "$1 $2")
    .replaceAll(joinToken, "");
};

arr.forEach((x) => console.log(smartJoin(x)));
// わたしはホットドッグがとても好きです。
// わたしはhotto dogguがとても好きです。
// I like hotto doggu very much.

追記 英単語と日本語はスペースで接続する

英単語と日本語の間にスペースを入れる場合は、smartJoinに以下の2行を追加しましょう。

 const smartJoin = (words: string[]) => {
   const joinToken = "__JOIN_TOKEN__";
   const joinAlnum = new RegExp(`(\\w)${joinToken}(\\w)`, "g");

   return words.join(joinToken)
     .replace(joinAlnum, "$1 $2")
     .replaceAll(joinToken, "")
+    .replace(/(\p{sc=Hiragana}|\p{sc=Katakana}|\p{sc=Han})([a-zA-Z]\w*)/gu, "$1 $2")
+    .replace(/([a-zA-Z]\w*)(\p{sc=Hiragana}|\p{sc=Katakana}|\p{sc=Han})/gu, "$1 $2");
 };
おまけunicode fonts対応

𝑢𝑛𝑖𝑐𝑜𝑑𝑒 𝑓𝑜𝑛𝑡𝑠は正規表現クラスでは対応しきれないので愚直に連結するしかなさそうです。

const unicodeFonts = [
  "𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳",
  "𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙",
  "𝑎𝑏𝑐𝑑𝑒𝑓𝑔ℎ𝑖𝑗𝑘𝑙𝑚𝑛𝑜𝑝𝑞𝑟𝑠𝑡𝑢𝑣𝑤𝑥𝑦𝑧",
  "𝐴𝐵𝐶𝐷𝐸𝐹𝐺𝐻𝐼𝐽𝐾𝐿𝑀𝑁𝑂𝑃𝑄𝑅𝑆𝑇𝑈𝑉𝑊𝑋𝑌𝑍",
  "𝒂𝒃𝒄𝒅𝒆𝒇𝒈𝒉𝒊𝒋𝒌𝒍𝒎𝒏𝒐𝒑𝒒𝒓𝒔𝒕𝒖𝒗𝒘𝒙𝒚𝒛",
  "𝑨𝑩𝑪𝑫𝑬𝑭𝑮𝑯𝑰𝑱𝑲𝑳𝑴𝑵𝑶𝑷𝑸𝑹𝑺𝑻𝑼𝑽𝑾𝑿𝒀𝒁",
  "𝖺𝖻𝖼𝖽𝖾𝖿𝗀𝗁𝗂𝗃𝗄𝗅𝗆𝗇𝗈𝗉𝗊𝗋𝗌𝗍𝗎𝗏𝗐𝗑𝗒𝗓",
  "𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹",
  "𝗮𝗯𝗰𝗱𝗲𝗳𝗴𝗵𝗶𝗷𝗸𝗹𝗺𝗻𝗼𝗽𝗾𝗿𝘀𝘁𝘂𝘃𝘄𝘅𝘆𝘇",
  "𝗔𝗕𝗖𝗗𝗘𝗙𝗚𝗛𝗜𝗝𝗞𝗟𝗠𝗡𝗢𝗣𝗤𝗥𝗦𝗧𝗨𝗩𝗪𝗫𝗬𝗭",
  "𝘢𝘣𝘤𝘥𝘦𝘧𝘨𝘩𝘪𝘫𝘬𝘭𝘮𝘯𝘰𝘱𝘲𝘳𝘴𝘵𝘶𝘷𝘸𝘹𝘺𝘻",
  "𝘈𝘉𝘊𝘋𝘌𝘍𝘎𝘏𝘐𝘑𝘒𝘓𝘔𝘕𝘖𝘗𝘘𝘙𝘚𝘛𝘜𝘝𝘞𝘟𝘠𝘡",
  "𝙖𝙗𝙘𝙙𝙚𝙛𝙜𝙝𝙞𝙟𝙠𝙡𝙢𝙣𝙤𝙥𝙦𝙧𝙨𝙩𝙪𝙫𝙬𝙭𝙮𝙯",
  "𝘼𝘽𝘾𝘿𝙀𝙁𝙂𝙃𝙄𝙅𝙆𝙇𝙈𝙉𝙊𝙋𝙌𝙍𝙎𝙏𝙐𝙑𝙒𝙓𝙔𝙕",
  "𝒶𝒷𝒸𝒹ℯ𝒻ℊ𝒽𝒾𝒿𝓀𝓁𝓂𝓃ℴ𝓅𝓆𝓇𝓈𝓉𝓊𝓋𝓌𝓍𝓎𝓏",
  "𝒜ℬ𝒞𝒟ℰℱ𝒢ℋℐ𝒥𝒦ℒℳ𝒩𝒪𝒫𝒬ℛ𝒮𝒯𝒰𝒱𝒲𝒳𝒴𝒵",
  "𝓪𝓫𝓬𝓭𝓮𝓯𝓰𝓱𝓲𝓳𝓴𝓵𝓶𝓷𝓸𝓹𝓺𝓻𝓼𝓽𝓾𝓿𝔀𝔁𝔂𝔃",
  "𝓐𝓑𝓒𝓓𝓔𝓕𝓖𝓗𝓘𝓙𝓚𝓛𝓜𝓝𝓞𝓟𝓠𝓡𝓢𝓣𝓤𝓥𝓦𝓧𝓨𝓩",
  "𝚊𝚋𝚌𝚍𝚎𝚏𝚐𝚑𝚒𝚓𝚔𝚕𝚖𝚗𝚘𝚙𝚚𝚛𝚜𝚝𝚞𝚟𝚠𝚡𝚢𝚣",
  "𝙰𝙱𝙲𝙳𝙴𝙵𝙶𝙷𝙸𝙹𝙺𝙻𝙼𝙽𝙾𝙿𝚀𝚁𝚂𝚃𝚄𝚅𝚆𝚇𝚈𝚉",
  "𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷",
  "𝔄𝔅ℭ𝔇𝔈𝔉𝔊ℌℑ𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔ℜ𝔖𝔗𝔘𝔙𝔚𝔛𝔜ℨ",
  "𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫",
  "𝔸𝔹ℂ𝔻𝔼𝔽𝔾ℍ𝕀𝕁𝕂𝕃𝕄ℕ𝕆ℙℚℝ𝕊𝕋𝕌𝕍𝕎𝕏𝕐ℤ",
  "𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫",
  "𝔸𝔹ℂ𝔻𝔼𝔽𝔾ℍ𝕀𝕁𝕂𝕃𝕄ℕ𝕆ℙℚℝ𝕊𝕋𝕌𝕍𝕎𝕏𝕐ℤ",
  "𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗𝟎",
  "𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿𝟶",
  "𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫𝟢",
  "𝟭𝟮𝟯𝟰𝟱𝟲𝟳𝟴𝟵𝟬",
  "𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡𝟘",
].join("");

export const smartJoin = (words: string[]) => {
  const joinToken = "__JOIN_TOKEN__";
  const alnumToken = `([${unicodeFonts}\\w])`;
  const joinAlnum = new RegExp(`${alnumToken}${joinToken}${alnumToken}`, "g");

  return words.join(joinToken)
    .replace(joinAlnum, "$1 $2")
    .replaceAll(joinToken, "");
};

Discussion