📝

条件の数が可変なArray.filter()をやりたかったポエム

2020/11/17に公開3

動機

ダッシュボードみたいなの作ってて、フィルタリング条件が増えたり減ったりして実装がめんど臭い事が多々あるのであらがってみたかった。MongoのQueryみたいなの妄想したけど、andとかorの条件ネストは諦めた。

そんなに続いてるわけでもないけどコレの続き。

やりたかった事

これに

var data = [
    {b: 500, g:{value: 30}},
    {b: 200},
    {b: 100},
    {b: 300},
    {string: "aiue", g:{value: 200}},
    {string: "aiueo", g:{value: 300}},
]

これを

var conditionParams = [
    {type: "match", "key": ["string"], "value": "ueo"},
    {type: "lt", "key": ["g", "value"], "value": 400},
]

こうすると

oreoreFilter(data, conditionParams, genCondition, true);

ANDでフィルタリングされてこうなる。

// -> [ {string: "aiueo", g:{value: 300}} ]

ゆるふわ実装

// ネストされたオブジェクトをキーで取り出すやつ
function search(data,keys){
    console.log(data, keys);
    return keys.reduce((current, key) => {
        try{
            return current[key]
        } catch(e) {
            return undefined
        }
    }, data)
}

// 検索条件を組み立てるやつ
// あとで挙動を変更したり追加できるようにミドルウェアみを持たせとく
function genCondition(condition){
    try{
        switch(condition.type){
            case "eq": return (value)=>{ return search(value, condition.key) === condition.value;  }
            case "gt": return (value)=>{ return search(value, condition.key) > condition.value; }
            case "lt": return (value)=>{ return search(value, condition.key) < condition.value; }
            case "ge": return (value)=>{ return search(value, condition.key) >= condition.value; }
            case "le": return (value)=>{ return search(value, condition.key) <= condition.value;  }
            case "match": return (value)=>{ return new RegExp(`.*${condition.value}.*`).test(search(value, condition.key)); }
            default: return ()=> false;
        }
    } catch (e){
        return false;
    }
}

// フィルタリング処理
// 対象の配列に対して実際にフィルタリングする関数
function oreoreFilter(data, conditionParams, genCondition, flg){
    const conditions = conditionParams.map(genCondition)

    return data.filter((value)=>{
        return conditions.reduce(
            (sum, condition)=>{       
                // 申し訳程度のAND/OR要素
                if (flg){
                    return sum && condition(value)
                } 
                return sum || condition(value)
            }
        ,flg)
    })
}

テスト

var data = [
    {b: 500, g:{value: 30}},
    {b: 200, g:{value: 500}},
    {b: 200, g:{value: 300}},
    {b: 100},
    {b: 300},
    {string: "aiue", g:{value: 200}},
    {string: "aiueo", g:{value: 500}},
]


var conditionParams = [
    {type: "gt", "key": ["b"], "value": 100}, 
    {type: "lt", "key": ["b"], "value": 700},
    {type: "lt", "key": ["g", "value"], "value": 400},
]

// AND検索
oreoreFilter(data, conditionParams, genCondition, true);
// ->  [ {b: 500, g:{value: 30}},    {b: 200, g:{value: 300}} ]

// OR検索
oreoreFilter(data, conditionParams, genCondition, false);
// ->  [
//         {b: 500, g:{value: 30}},
//         {b: 200, g:{value: 500}},
//         {b: 200, g:{value: 300}},
//         {b: 100},
//         {b: 300},
//         {string: "aiue", g:{value: 200}},
//      ]

まとめ

とりあえず動いてる気がする。

Discussion

standard softwarestandard software

単純な、多重フィルタで関数を指定したほうが、応用が効くような気がしました。

const oreoFilter = (data, paramFunctions) => {
  let result = [...data];
  for (const func of paramFunctions) {
    result = result.filter(func)
  }
  return result;
}

console.log(oreoFilter(data, [
  (value) => {
    return isUndefined(value.string) ? false 
      : value.string.includes('ueo')
  },
  (value) => {
    const result = search(value, ['g', 'value']);
    return isUndefined(result) ? false 
      : result < 400
  },
]));

ざくっとした動作コードです。
https://jsbin.com/hilokucoro/edit?html,js,console

okd.shokd.sh

こちらのコードの方が素直に見通しが良くて良さそうですね。
参考になります!

nap5nap5

ネストとかを考慮したいときは木構造のデータ構造をうまく取り入れたら達成しやすいかもです。

定義側

type Predicate<T> = (data: T) => boolean;

type Logic = "AND" | "OR";

type LogicNode<T> = {
  logic: Logic;
  children: FilterCondition<T>[];
}

export type FilterCondition<T> = LogicNode<T> | Predicate<T>;

const evaluateCondition = <T>(condition: FilterCondition<T>) => (data: T): boolean => {
  if (typeof condition === 'function') {
    return (condition as Predicate<T>)(data);
  }
  const logicNode: LogicNode<T> = condition;
  if (logicNode.logic === "AND") {
    return logicNode.children.every(child => evaluateCondition(child)(data));
  }
  return logicNode.children.some(child => evaluateCondition(child)(data));
}

export const filterData = <T>(condition: FilterCondition<T>) => (data: T[]) => {
  return data.filter(evaluateCondition(condition));
}

使用側

test('年齢が25歳未満で、住所が大阪府または福岡県のユーザー', () => {
  const condition: FilterCondition<User> = {
    logic: 'AND',
    children: [
      (user: User) => user.age < 25,
      {
        logic: 'OR',
        children: [
          (user: User) => user.address.prefecture === '大阪府',
          (user: User) => user.address.prefecture === '福岡県',
        ],
      },
    ],
  }

  const outputData = filterData(condition)(users)
  expect(outputData.map((d) => omit(d, ['lastLogin']))).toStrictEqual([
    {
      id: '10',
      email: '田中@example.jp',
      age: 20,
      name: '田中 五郎',
      isAdmin: false,
      // lastLogin: '2023-02-20T15:00:00.000Z',
      address: {
        id: 'a10',
        country: '日本',
        prefecture: '福岡県',
        city: '中央区',
      },
    },
    {
      id: '28',
      email: '渡辺@example.jp',
      age: 22,
      name: '渡辺 次郎',
      isAdmin: true,
      // lastLogin: '2023-02-27T15:00:00.000Z',
      address: {
        id: 'a28',
        country: '日本',
        prefecture: '福岡県',
        city: '中央区',
      },
    },
    {
      id: '40',
      email: '鈴木@example.jp',
      age: 21,
      name: '鈴木 五郎',
      isAdmin: false,
      // lastLogin: '2023-03-07T15:00:00.000Z',
      address: {
        id: 'a40',
        country: '日本',
        prefecture: '大阪府',
        city: '北区',
      },
    },
  ])
})

demo code.
https://codesandbox.io/p/sandbox/dazzling-goldwasser-n56sp6?file=/src/index.ts:1,1