目的
画面上に文字列を表示するときに、長すぎる場合は末尾を切り詰めて、さらに続きがあるよということを示すために「...」等を付加したい時があります。substr
が使えそうですが、JavaScriptのsubstr
はマルチバイト対応してしまっているがために、「あいうえお」でも「abcde」でも同じ5文字とカウントされ、全角のみの場合と半角のみの場合では同じ文字数でも文字列の表示幅が倍近く違うということになります。全角文字は半角文字の倍でカウントして、例えば5文字で切り詰める場合に、「abcde」はそのまま「abcde」、「あいうえお」は「あ...」のように、「...」分を考慮して必要に応じて文字を切り詰め、結果として半角文字分と同程度の幅の文字列を得たいと思います。説明がややこしいですが、要はRubyのtruncate
と同じような関数がほしいと思いました。
実装
const truncate = function(string, length, omission = '...') {
return (string.match(/[^ -~]/g)?.length ?? 0) + string.length <= length ?
string :
string.split('')
.reduce(
(acc, c) => (acc[1] += !c.match(/[ -~]/) + 1) <= length - omission.length ?
[acc[0] + c, acc[1]] :
[acc[0], length],
['', 0]
)[0] + omission;
}
2行目の条件は全角を2半角を1と数えて文字列長が指定の長さを超えているかどうかを判定しています。超えていなければ何もする必要はないのでそのまま返すのが3行目。4行目以降が本体ですが、まずsplit
で文字列を文字配列にしてそれをreduce
していきます。アキュムレータとして今までカウントした文字数とそこまでの部分文字列を渡していくので配列になっているのがややダサいところでしょうか。reduce
内のmatch
は文字毎に全角なら2、半角なら1をアキュムレータに足し込んでいく処理です。ここも2行目で全体にmatch
かけてるのに、個々の文字にも同じようなmatch
をするのがちょっとなぁ、と思いますが…。あとはまだ指定の長さに達していなければその文字を足し込み、指定の長さを超えるようなら足さない、という判断をしてグルグル回します。reduce
の結果の先頭要素が目的の文字列ですから、それに省略したことを表す文字列を付加してtruncate
の結果とします。