見出し画像

条件に基づいた連続値のグループ化をTypescriptで実現するやり方

こんにちわ。nap5です。


条件に基づいた連続値のグループ化をTypescriptで実現するやり方の紹介です。


Tableauで紹介されているナレッジをTypescriptでやってみましたといった内容になります。


https://kb.tableau.com/articles/HowTo/running-count-groups?lang=ja


ナレッジに紹介されているシナリオをもとにやってみました。

「利益がマイナスだった連続日数に基づいて、日のグループを作成」

このシナリオを達成するために以下の手順を踏みます。

  1. 日付が連続しているかを判定する関数の作成

  2. predicate関数を引数にとるpartitionBy関数を作成

  3. シナリオに応じてグループ化する条件式を定義

  4. 入力データを関数に与えて期待する出力を確認

まずは「日付が連続しているかを判定する関数の作成」になります。

import { default as dayjs } from 'dayjs'
import ja from 'dayjs/locale/ja'
import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'

export type InputDay = dayjs.Dayjs | string | Date | number | null | undefined

dayjs.extend(timezone)
dayjs.extend(utc)
dayjs.locale(ja)
dayjs.tz.setDefault('Asia/Tokyo')

export const areConsecutiveDates = (date1: InputDay, date2: InputDay): boolean => {
  return dayjs(date2).diff(dayjs(date1), "days") === 1;
};


次に「predicate関数を引数にとるpartitionBy関数を作成」になります。

export type TRow = Record<string, unknown>;

export const partitionByWithPredicate = <T extends TRow>(
  data: T[],
  predicate: (prev: T, curr: T) => boolean
): T[][] => {
  return data.reduce((acc: T[][], row: T) => {
    if (
      acc.length > 0 &&
      predicate(
        acc[acc.length - 1][acc[acc.length - 1].length - 1],
        row
      )
    ) {
      // 現在の行と直前のパーティションの最後の行がpredicateに基づいて同じ場合に
      // 直前のパーティションに現在の行を追加する
      acc[acc.length - 1].push(row);
    } else {
      // 新しいパーティションを作成し、現在の行を追加する
      acc.push([row]);
    }
    return acc;
  }, []);
};


ここからは使用側になります。

「シナリオに応じてグループ化する条件式を定義」し、

  type ProfitData = {
    date: string;
    profit: number;
  };

  const partitionByNegativeProfit = (data: ProfitData[]): ProfitData[][] => {
    return partitionByWithPredicate(data, (prev, curr) => {
      return (
        areConsecutiveDates(prev.date, curr.date) &&
        prev.profit < 0 &&
        curr.profit < 0
      );
    });
  };


最後にテストコードを書いて期待する出力を得ることができているか確認します。

import { describe, test, expect } from "vitest";
import { bind, bindTo, map } from "fp-ts/lib/Identity";
import { pipe } from "fp-ts/lib/function";
import { partitionByWithPredicate } from "./utils/dataUtil";
import { areConsecutiveDates } from "./utils/dateUtil";

// @see https://kb.tableau.com/articles/HowTo/running-count-groups?lang=ja
describe("条件に基づいた連続値のグループ化", () => {
  type ProfitData = {
    date: string;
    profit: number;
  };

  const inputData: ProfitData[] = [
    { date: "2023-10-01", profit: 10 },
    { date: "2023-10-02", profit: -5 },
    { date: "2023-10-03", profit: -7 },
    { date: "2023-10-04", profit: -6 },
    { date: "2023-10-05", profit: 8 },
    { date: "2023-10-06", profit: -3 },
    { date: "2023-10-07", profit: 10 },
    { date: "2023-10-10", profit: -9 },
    { date: "2023-10-11", profit: -7 },
    { date: "2023-10-12", profit: -6 },
    { date: "2023-11-1", profit: -1 },
    { date: "2023-11-2", profit: -7 },
    { date: "2023-11-5", profit: -6 },
  ];

  const partitionByNegativeProfit = (data: ProfitData[]): ProfitData[][] => {
    return partitionByWithPredicate(data, (prev, curr) => {
      return (
        areConsecutiveDates(prev.date, curr.date) &&
        prev.profit < 0 &&
        curr.profit < 0
      );
    });
  };

  test("マイナス利益が3日間以上続いた連続データに基づくグループ化", () => {
    const outputData = pipe(
      inputData,
      bindTo("input"),
      bind("partitioned", ({ input }) => partitionByNegativeProfit(input)),
      bind("output", ({ partitioned }) =>
        partitioned.filter((g) => g.length >= 3)
      ),
      map(({ output }) => output)
    );

    expect(outputData).toStrictEqual([
      [
        {
          date: "2023-10-02",
          profit: -5,
        },
        {
          date: "2023-10-03",
          profit: -7,
        },
        {
          date: "2023-10-04",
          profit: -6,
        },
      ],
      [
        {
          date: "2023-10-10",
          profit: -9,
        },
        {
          date: "2023-10-11",
          profit: -7,
        },
        {
          date: "2023-10-12",
          profit: -6,
        },
      ],
    ]);
  });

  test("マイナス利益が2日間以上続いた連続データに基づくグループ化", () => {
    const outputData = pipe(
      inputData,
      bindTo("input"),
      bind("partitioned", ({ input }) => partitionByNegativeProfit(input)),
      bind("output", ({ partitioned }) =>
        partitioned.filter((g) => g.length >= 2)
      ),
      map(({ output }) => output)
    );

    expect(outputData).toStrictEqual([
      [
        {
          date: "2023-10-02",
          profit: -5,
        },
        {
          date: "2023-10-03",
          profit: -7,
        },
        {
          date: "2023-10-04",
          profit: -6,
        },
      ],
      [
        {
          date: "2023-10-10",
          profit: -9,
        },
        {
          date: "2023-10-11",
          profit: -7,
        },
        {
          date: "2023-10-12",
          profit: -6,
        },
      ],
      [
        {
          date: "2023-11-1",
          profit: -1,
        },
        {
          date: "2023-11-2",
          profit: -7,
        },
      ],
    ]);
  });
});



demo code.



簡単ですが、以上です。

いいなと思ったら応援しよう!