見出し画像

BIツールなどに見られるTopN件のデータ取得をTypescriptで実現するやり方

こんにちわ。nap5です。


BIツールなどに見られるTopN件のデータ取得をTypescriptで実現するやり方の紹介です。


定義側になります。

import { Path } from "dot-path-value";

export type TRow = Record<string, unknown>;

export type TRowWithNumber<T extends TRow> = T & Record<string, number>;

export const compare = <T extends TRow>(fields: Path<T>[], a: T, b: T): number => {
  for (const field of fields) {
    if (a[field] < b[field]) return -1;
    if (a[field] > b[field]) return 1;
  }
  return 0;
};

export const partitionBy = <T extends TRow>(data: T[], fields: Path<T>[]): T[][] =>
  data.reduce((acc: T[][], row: T) => {
    if (
      acc.length > 0 &&
      compare(
        fields,
        acc[acc.length - 1][acc[acc.length - 1].length - 1],
        row
      ) === 0
    ) {
      // 現在の行と直前のパーティションの最後の行が指定されたフィールドすべてで同じ場合に
      // 直前のパーティションに現在の行を追加する
      acc[acc.length - 1].push(row);
    } else {
      // 新しいパーティションを作成し、現在の行を追加する
      acc.push([row]);
    }
    return acc;
  }, []);

export const rowNumber = <T extends TRow>(
  data: T[][],
  alias: string = "rowNumber"
): TRowWithNumber<T>[][] =>
  data.map((d) =>
    d.map((row, index) => ({
      ...row,
      [alias]: index + 1,
    }))
  );

export const topN = <T extends TRowWithNumber<TRow>>(
  data: T[][],
  n: number,
  alias: string = "rowNumber"
): T[][] => data.map((d) => d.filter((row) => row[alias] <= n));


使用側になります。

import { test, expect, describe } from "vitest";
import { partitionBy, rowNumber, topN } from ".";
import { pipe } from "fp-ts/lib/function";
import { bind, bindTo, map } from "fp-ts/lib/Identity";
import { arrange, asc, desc, tidy } from "@tidyjs/tidy";

describe("TopN件のデモ", () => {
  test("カテゴリ別のTop3件", () => {
    type Demo = {
      id: number;
      name: string;
      category: string;
      score: number;
    };

    const data: Demo[] = [
      { id: 1, name: "Alice", category: "A", score: 100 },
      { id: 2, name: "Bob", category: "A", score: 105 },
      { id: 3, name: "Charlie", category: "A", score: 98 },
      { id: 4, name: "Derek", category: "A", score: 102 },
      { id: 5, name: "Eva", category: "A", score: 101 },
      { id: 6, name: "Frank", category: "A", score: 96 },
      { id: 7, name: "George", category: "B", score: 110 },
      { id: 8, name: "Helen", category: "B", score: 112 },
      { id: 9, name: "Ian", category: "B", score: 108 },
      { id: 10, name: "Jane", category: "B", score: 111 },
      { id: 11, name: "Kyle", category: "B", score: 107 },
    ];

    const result = pipe(
      data,
      bindTo("input"),
      bind("ordered", ({ input }) =>
        tidy(input, arrange([asc((d) => d.category), desc((d) => d.score)]))
      ),
      bind("partitioned", ({ ordered }) => partitionBy(ordered, ["category"])),
      bind("ranked", ({ partitioned }) => rowNumber(partitioned, "rank")),
      bind("output", ({ ranked }) => topN(ranked, 3, "rank")),
      map(({ output }) => output)
    );

    expect(result).toStrictEqual([
      [
        { id: 2, name: "Bob", category: "A", score: 105, rank: 1 },
        { id: 4, name: "Derek", category: "A", score: 102, rank: 2 },
        { id: 5, name: "Eva", category: "A", score: 101, rank: 3 },
      ],
      [
        { id: 8, name: "Helen", category: "B", score: 112, rank: 1 },
        { id: 10, name: "Jane", category: "B", score: 111, rank: 2 },
        { id: 7, name: "George", category: "B", score: 110, rank: 3 },
      ],
    ]);
  });
});


demo code.


簡単ですが、以上です。

この記事が気に入ったらサポートをしてみませんか?