条件に基づいた連続値のグループ化をTypescriptで実現するやり方
こんにちわ。nap5です。
条件に基づいた連続値のグループ化をTypescriptで実現するやり方の紹介です。
Tableauで紹介されているナレッジをTypescriptでやってみましたといった内容になります。
https://kb.tableau.com/articles/HowTo/running-count-groups?lang=ja
ナレッジに紹介されているシナリオをもとにやってみました。
「利益がマイナスだった連続日数に基づいて、日のグループを作成」
このシナリオを達成するために以下の手順を踏みます。
日付が連続しているかを判定する関数の作成
predicate関数を引数にとるpartitionBy関数を作成
シナリオに応じてグループ化する条件式を定義
入力データを関数に与えて期待する出力を確認
まずは「日付が連続しているかを判定する関数の作成」になります。
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.
簡単ですが、以上です。