この記事の目次
一覧からタグを集めたら、「木工」が何度も並んでしまった。Setで重複するタグを整理するには、まず何を同じタグとみなすかを決めます。見た目が似ている文字を自動ですべて一つにする処理ではありません。
題材は架空のものづくり資料集です。配列と関数を少し読める人に向けて、タグの選択肢に使う文字列の配列を作ります。画面への描画、クリックによる絞り込み、出現回数の集計は含めません。Node.jsか、自分の練習ページの開発者ツールを実行場所にします。
残したいのは資料の件数ではなく、タグの種類
入力を「木工、染色、木工、紙、染色」とします。資料に付いていたタグは五つありますが、選択肢として必要なのは「木工、染色、紙」の三種類です。元の五つを削除するのではなく、選択肢用の別の結果を作ります。
ここでは最初に現れた順序を残すことにします。五つを左から見て、まだない名前なら追加し、既にある名前なら追加しない、という判断です。「染色」を先頭へ並べ替える操作や、人気順にする判断は入れません。
Setは同じ値を重ねて持たない集合です。MDNのSetの説明では、値の一意性と追加順で取り出せることを確認できます。今回使う値は文字列だけに限定し、文字が同じタグを一度ずつ残します。
重要なのは「重複が消えた」という見た目より、残した順序と同一視した範囲を説明できることです。選択肢には三種類でよくても、元の資料が何件あったかを調べる処理には五つの入力が必要です。元データと用途別の結果を分ける理由を、先に確認しておきましょう。
Setを作り、使いやすい配列へ戻す
次をunique-tags.jsという新しいファイルへ保存し、同じフォルダでnode unique-tags.jsを実行します。コンソールでも一まとまりで実行できます。tagsには、欠けた要素のない文字列配列を渡す前提です。
function uniqueTags(tags) {
return [...new Set(tags)];
}
const tags = ["木工", "染色", "木工", "紙", "染色"];
const choices = uniqueTags(tags);
console.log(choices);
console.log(choices.length);
console.log(tags);
期待する結果は、順に「木工・染色・紙」を持つ配列、数値の3、元の五つのタグを持つ配列です。new Set(tags)でタグを集合に入れ、外側の角括弧と三つの点で集合の値を新しい配列へ展開しています。この三つの点をスプレッド構文と呼びます。
配列へ戻すのは、以降の表示処理へ渡す選択肢を配列で受け渡す、という今回の約束のためです。Set自体を使いたい処理なら、そのまま保持する方法もあります。集合を作ることと、配列として渡すことを別の段階として読んでください。
この関数は入力へ代入したり、要素を削除したりしていません。元のtagsは五つのままです。結果へ「織物」を追加しても元の配列には追加されないことを試せます。ただし今回は文字列しか扱っておらず、あらゆる入れ子のデータを複製する方法として説明しているわけではありません。
同じに見えるタグが残ったら、表記を先に比べる
一つ目の迷いは、「木工」と「 木工 」が二つ残ることです。後者には前後の空白があるので、同じ文字列ではありません。Setの故障と考える前に、入力を引用符付きで読み比べます。
今回の資料集では「タグの前後の空白は入力時の余分な空白」と決めた場合だけ、それを除いてから重複を整理します。次は最初のコードとは別に実行できる追加例です。空白だけの値は選択肢に残さない方針も明示しています。
function cleanTagChoices(rawTags) {
const cleaned = rawTags.map((tag) => tag.trim());
const named = cleaned.filter((tag) => tag !== "");
return [...new Set(named)];
}
console.log(cleanTagChoices(["木工", " 木工 ", " ", "紙"]));
console.log(cleanTagChoices(["紙 工作", "紙工作", "DIY", "diy"]));
最初の結果は「木工・紙」の二つ、次の結果は入力と同じ四つです。trimは前後の空白を除くための処理で、間の空白や英字の大小はこの例では変更しません。詳しい入力の境界はtrimの練習記事を参照できます。
修正後に確かめるのは、余分な空白のある木工が一つになること、空の選択肢が消えること、区別を残すと決めた四つが統合されないことです。「全部小文字にすればよい」と処理を広げず、区別する必要のある名前がないかを先に考えます。
処理順も説明できます。先にSetへ入れてから空白を除くと、異なる値として残った二つが、後からどちらも木工になって再び重複します。同じとみなすための整形を済ませ、その結果に対して一意性を確かめる順序にします。
オブジェクトをそのまま渡すと、同じ名前でも別物になる
二つ目の失敗は、データをオブジェクトに変えた後も同じ関数で十分だと考えることです。次の例は問題を観察するために単独で実行できます。
const records = [{ tag: "木工" }, { tag: "木工" }];
console.log(new Set(records).size); // 2
const names = records.map((record) => record.tag);
console.log([...new Set(names)]); // ["木工"]
別々に作った二つのオブジェクトは、tagの中身が同じでも同じ参照ではありません。Setはオブジェクト内部の全項目を見比べて同一にするものではないため、最初の件数は2です。
今回は欲しい結果がタグ名だけなので、先に文字列を取り出してから整理します。再確認は、結果が木工一つになり、元のrecordsが二件のままであることです。mapで表示用の文字列を作る記事と組み合わせる場合も、まず何を一つの値として比べたいかを決めてください。
同じ名前でも別の分類番号を持つ資料を、丸ごと一件へ統合したい場合は、この修正では足りません。どの番号を残すか、別の説明を捨ててよいかという判断が増えます。オブジェクトの重複排除へ無条件に流用せず、名前の選択肢を作る範囲にとどめます。
重複だけが消え、順序と大文字・小文字は残るか
最初のuniqueTagsを使い、入力と出力を紙へ書いてから実行します。次の四つが期待結果になります。
- 空配列を渡すと空配列になり、架空のタグは補われない。
- 木工だけを三つ渡すと、木工一つになる。
- 紙、木工、紙の順に渡すと、紙、木工の順に残る。
- DIYとdiyを渡すと、今回の規則では二つとも残る。
次にcleanTagChoicesで、空白だけの三つの文字列が空配列になることも確認します。入力が文字列以外ならtrimをそのまま呼べないため、外部データを扱う実装では入口で形式を確かめる処理が別に必要です。
出現回数を知りたいなら、Setで種類を残すだけでは答えが失われます。選択肢と件数集計を混同しないことが、今回の使い分けです。JavaScriptの無料レッスンへ進む際は、結果の三件が「資料三件」ではなく「タグ三種類」を意味する、と説明できることを確かめましょう。
記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。