0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

一覧の並び順を最後まで決定的にする — 同じデータなら同じ HTML が出る

0
Posted at

こんにちは。小学生向けのニュースサイト、こどもニュースをつくっています。

このサイトは 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 を許す項目は、比較でどちらに倒すかを明示する
  • 同じ順序を使う画面は関数を共有する。目的が違う順序(機械向け)は分ける
0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?