こんにちは。小学生向けのニュースサイト、こどもニュースをつくっています。
このサイトは 1 日にいくつか記事が出ます。日付ごとのページに並ぶので、並び順を決める必要があります。
素朴には「新しい順」ですが、同じ日の記事なので時刻の差に意味がありません。読者にとって良い順序を考えて、結果的に条件が 4 段階になりました。
何を優先するか
読者が最初に見る一覧なので、入口として強い記事を前に出したいところです。
このサイトの記事には、画像が付くものと付かないものがあります。教科書の単元タグが付くものと付かないものもあります。どちらも「読みたくなる度合い」に効きます。
優先順位はこうしました。
画像とタグ(トピックス)が揃っている記事ほど読者の入口として強いので前に出す。
画像の有無がタグの有無より優先する(見出しの見た目が変わるため):
1. タグあり + 画像あり
2. タグなし + 画像あり
3. タグあり + 画像なし
4. 両方なし
画像を優先しているのは、見出しの見た目が変わるからです。画像がある記事は一覧でも目に留まります。タグは補助的な情報なので、次点にしました。
実装は、2 つの真偽値を順に比べるだけです。
const byImage = compareBoolDesc(a.hasImage, b.hasImage);
if (byImage !== 0) return byImage;
const byHasTag = compareBoolDesc(a.tagCount > 0, b.tagCount > 0);
if (byHasTag !== 0) return byHasTag;
4 段階の優先度を、2 つの比較で表しています。組み合わせを列挙して数値のスコアにする方法もありますが、条件が増えたときに読めなくなります。順に比べる形なら、優先順位がそのままコードの順序になります。
同点をどう解くか
ここからが本題です。
同点のときの扱いを決めないと、並び順が実行のたびに変わる可能性があります。JavaScript の sort は現在の仕様では安定ソートですが、そもそも入力の順序が保証されていなければ意味がありません。データベースから取ってくる順序は、クエリに ORDER BY が無ければ不定です。
なので、最後まで決めました。
// 3) タグが多い順
if (a.tagCount !== b.tagCount) return b.tagCount - a.tagCount;
// 4) 確認したのが古い順(未確認は最後)
if (a.verifiedAt !== b.verifiedAt) {
if (a.verifiedAt === null) return 1;
if (b.verifiedAt === null) return -1;
return a.verifiedAt.localeCompare(b.verifiedAt);
}
さらに、作成時刻と id まで比べます。
最後に created_at → id で完全な決定性を持たせる(同じ DB なら常に同じ並び=ビルド出力が安定)。
id は一意なので、ここまで来れば必ず順序が決まります。
なぜそこまでするか
理由は 3 つあります。
1 つ目は、ビルド出力を比較できるようにするためです。このサイトでは、リファクタのときに「変更前後の全 HTML を diff する」検証をしています。並び順が不定だと、意味のない差分が出て検証が使えません。
2 つ目は、デプロイのたびに順序が変わらないようにするためです。読者がブックマークしたページの内容が、更新のたびに入れ替わるのは気持ちが悪いです。
3 つ目は、バグの再現性です。順序に依存した不具合が出たときに、同じ入力で同じ結果になるなら追えます。不定だと「たまに起きる」になります。
決定的にするコストは、比較関数の最後に 2 行足すだけでした。得られるものと比べると、明らかに安いです。
null の扱いを決める
確認時刻は、公開している記事には必ず入りますが、型としては null を許します。
/** 人間が確認した時刻。公開記事は必ず入るが、欠損時は最後に回す。 */
verifiedAt: string | null;
null のときにどうするかを決めておかないと、比較の結果が不定になります。ここでは「最後に回す」と決めて、明示的に書きました。
null を含む比較は、書き忘れるとエラーにならずに順序だけ壊れます。型で表現されているなら、必ず分岐を書くようにしています。
適用する場所を揃える
この比較関数は、複数の画面で使っています。日付ごとのページ、トップの日別カード、最新のフィードです。
同じ日の記事が、画面によって違う順序で並んでいると混乱します。関数を共有していれば、それが起きません。
一方、sitemap やデータのエクスポートでは別の順序(日付と作成順)を使っています。こちらは読者向けの並びではなく、機械が読むための安定した順序が要るためです。
「順序」と一口に言っても、目的が違えば別のものです。共有するのは目的が同じものだけにしました。
まとめ
- 一覧の優先順位は、真偽値を順に比べる形で書くと、優先順位がコードの順序として読める
- 同点の解き方を最後まで決めて、id まで比べる。同じデータなら必ず同じ並びになる
- 決定的にすると、出力の比較検証・デプロイの安定・バグの再現性が得られる
- null を許す項目は、比較でどちらに倒すかを明示する
- 同じ順序を使う画面は関数を共有する。目的が違う順序(機械向け)は分ける