はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

JavaScriptでもmapやreduce、filter(手習いメモ)

今更ですが、JavaScript(私がいるのはTypeScriptでもなくて、やっとES6ぐらいに合流した世界線です)にもmapやreduceがあることを知りまして、世間からの何周遅れを追いつきたく、手習いしてみた...という自分メモです。

個人的なmapやreduceあるいはこれらと相性の良い便利な関数の使い所を自分の中で再確認してみたものを、特に深く考えずに詰め込みしています。

無駄に卑下するものではありませんが、特別なことは書いてない私的なメモでございます。

ただ、JavaScriptのmapやreduceやこれらの眷属の関数は、なんと配列の要素はもちろん、現在の要素の添字や配列全体を取得できるというこれまた便利な味付けがされているので、多少、添字が使えると、伝統的なmapよりも、ちょっとしたことができるな〜と思った例に多少は寄せてあるつもりです。

組み合わせ(ぽいやつ)

組み合わせを作る

/* 複数の配列を与えて、各配列の要素の全ての組み合わせの「組合せ」を生成する */

// サブ関数1
// [1,2,3] と 4 -> [1,4],[2,4],[3,4]
const tuples = (xa, y) => xa.map(x => [x, y].flat());

// サブ関数2
// [1,2,3] と [4,5,..] -> [1,4],[1,5],...,[2,4],[2,5]
const tupless = (xa, ya) => ya.map(y => tuples(xa, y)).flat();

// メイン関数
// [1,2,...][4,5,..][8,9,...] -> [1,4,8],[1,5,8],...
const get_comb = (a_a) => a_a.reduce((xarr, yarr) => tupless(xarr, yarr));

// サンプルデータを食わせる〜出力して確認

const a = ['1', '2', '3'];
const b = ['4', '5', '6'];
const c = ['8', '9', '10', '11', '12'];
const a_a = [a, b, c];

const comb = get_comb(a_a);

comb.forEach(i => {
    console.log(i.join('---'));
});

組み合わせを間引く

/* 組合せから、特定の組合せのパターンを間引く */

const same = (a, ba) => ba.map(b => JSON.stringify(b) === JSON.stringify(a)).some(Boolean);
const combf = comb.filter(v => (!same(v, [['1', '4', '8'], ['2', '4', '8']])));
// 注:combは先述の例参照

combf.forEach(i => {
    console.log(i.join('---'));
});

ネストされたJSONデータ

/* Elasticsearchのaggs(2階層型)の戻り値(風のデータ)から、組み合わせのタプルを抜き出す */
// ↓ livedoorのレストランデータ由来のデータ
const aggs = {
    "a1": {
        "doc_count_error_upper_bound": 0,
        "sum_other_doc_count": 0,
        "buckets": [
            {
                "key": "13__東京都",
                "doc_count": 64565,
                "a2": {
                    "doc_count_error_upper_bound": 649,
                    "sum_other_doc_count": 59946,
                    "buckets": [
                        {
                            "key": "和食",
                            "doc_count": 23243
                        },
                        {
                            "key": "居酒屋・ダイニングバー",
                            "doc_count": 13153
                        },
                        {
                            "key": "西洋料理",
                            "doc_count": 10980
                        }
                    ]
                }
            }
        ]
    }
}

const bk = aggs.a1.buckets;

const shapedBucketAsMap = (bk, af) => bk.reduce((a, c) => { a[c.key] = c[af].buckets; return a; }, {});
let buckinfo = shapedBucketAsMap(bk, 'a2');
console.log(buckinfo);

const nestbuc2Tuple = (bk, af) => bk.map(v1 => v1[af].buckets.map(v2 => [v1.key, v2.key]));
buckinfo = nestbuc2Tuple(bk, 'a2').flat();
console.log(buckinfo);

↓ 出力イメージ

> console.log(buckinfo);

{
  '13__東京都': [
    { key: '和食', doc_count: 23243 },
    { key: '居酒屋・ダイニングバー', doc_count: 13153 },
    { key: '西洋料理', doc_count: 10980 }
  ]
}
undefined
> 

> const nestbuc2Tuple = (bk, af) => bk.map(v1 => v1[af].buckets.map(v2 => [v1.key, v2.key]));
undefined
> buckinfo = nestbuc2Tuple(bk, 'a2').flat();
[
  [ '13__東京都', '和食' ],
  [ '13__東京都', '居酒屋・ダイニングバー' ],
  [ '13__東京都', '西洋料理' ]
]
> console.log(buckinfo);
[
  [ '13__東京都', '和食' ],
  [ '13__東京都', '居酒屋・ダイニングバー' ],
  [ '13__東京都', '西洋料理' ]
]

GroupBy

/* オブジェクトの配列を各オブジェクトのあるプロパティでGroupby */
const group = (objArr, getKeyFunc) =>
    objArr.reduce((grp, o, i, s) => {
        const gname = getKeyFunc(o, i, s);
        (grp[gname] || (grp[gname] = [])).push(o);
        return grp;
    }, Object.create(null));

const data = [
    { id: '111', name: 'A', p: 100 },
    { id: '222', name: 'B', p: 50 },
    { id: '333', name: 'C', p: 100 },
    { id: '444', name: 'C', p: 50 },
    { id: '555', name: 'D', p: 70 },
];

const getfn = (o, i, s) => 'グループ_' + o.name;
console.log(group(data, getfn));

↓ 出力イメージ

 {
  'グループ_A': [ { id: '111', name: 'A', p: 100 } ],
  'グループ_B': [ { id: '222', name: 'B', p: 50 } ],
  'グループ_C': [ { id: '333', name: 'C', p: 100 }, { id: '444', name: 'C', p: 50 } ],
  'グループ_D': [ { id: '555', name: 'D', p: 70 } ]
}

配列のランダム化

/* 配列の並びのランダム化 */
// ただし、Math.randomそのもののエントロピーはここでは十分なものとみなしてください。
const randomize = arr_ => {
    const arr = [...arr_];
    return arr_.map(() => arr.splice(Math.floor(Math.random() * arr.length), 1)[0]);
};

> randomize([1,2,3,4,5,6])
[ 6, 3, 2, 4, 5, 1 ]
> randomize([1,2,3,4,5,6])
[ 6, 3, 4, 1, 5, 2 ]
> randomize([1,2,3,4,5,6])
[ 1, 6, 2, 5, 3, 4 ]
> randomize([1,2,3,4,5,6])
[ 3, 1, 6, 5, 2, 4 ]
> randomize([1,2,3,4,5,6])
[ 4, 6, 1, 2, 5, 3 ]
> 

Trie風のデータ

/* trie 風のデータ構造を元の文字にに復元する */
// {'a':['b','bc','bcb','d'],..} --> [['ab','abc','abcb','ad'],...]
function decode_trie1(a) {
    return Object.keys(a).map(k => a[k].map(item => `${k}${item}`));
}

decode_trie1({ 'a': ['b', 'bc', 'bcb', 'd'], 'c': ['d'] })

/* trie 風のデータ構造を元の文字にに復元するその2 */
// {'a':['b','bc','bcb','d'],..} --> {a:['ab','abc','abcb','ad'], ...}
function decode_trie2(a) {
    return Object.keys(a).reduce((acm, c) => {
        acm[c] = a[c].map(item => `${c}${item}`);
        return acm;
    }, Object.create(null));
}

decode_trie2({ 'a': ['b', 'bc', 'bcb', 'd'], 'c': ['d'] })

配列のチャンク分割

/* 配列のチャンク分割 */
const chunksize = 3;
const renban = [...Array(chunksize * 10)].map((_, i) => i); // mapに引数
const chunk = (arr, size) => {
    return arr.reduce((a, c, i) => {
        (i % size === 0)
            ? a.push([c])
            : a[a.length - 1].push(c);
        return a;
    }, []);
};

chunk(renban, chunksize);

> chunk(renban, chunksize);
[
  [ 0, 1, 2 ],
  [ 3, 4, 5 ],
  [ 6, 7, 8 ],
  [ 9, 10, 11 ],
  [ 12, 13, 14 ],
  [ 15, 16, 17 ],
  [ 18, 19, 20 ],
  [ 21, 22, 23 ],
  [ 24, 25, 26 ],
  [ 27, 28, 29 ]
]
> 

配列の要素のユニーク化(setだと難しいもの)


/* 順番を維持しつつユニーク: 最初に現れたものを残す (srcとiがあるので便利) */
[1, 3, 1, 2, 4, 5, 1, 6, 5, 6, 8, 9, 1].filter((el, i, src) => src.indexOf(el) === i);

/* 最後残しユニーク(srcとiがあるので便利) */
[1, 3, 1, 2, 4, 5, 1, 6, 5, 6, 8, 9, 1].filter((el, i, src) => src.indexOf(el, i + 1) === -1);

/* 最後残しユニークをreverseを使って実現 */
[1, 3, 1, 2, 4, 5, 1, 6, 5, 6, 8, 9, 1].reverse().filter((el, i, src) => src.indexOf(el) === i).reverse();

/* reverseを独自実装(reduceがシンプルかも) ※言語の標準は破壊的だが、これはそうではない */
// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse
[1, 3, 1, 2, 4, 5, 1, 6, 5, 6, 8, 9, 1].reduce((acm, c) => [c].concat(acm));

/* 重複しているものを残す */
[1, 3, 1, 2, 4, 5, 1, 6, 5, 6, 8, 9, 1].filter((el, i, src) =>
    src.slice(0, i ? i - 1 : 0).indexOf(el) > -1
    || src.slice(i + 1).indexOf(el) > -1
);


everyとsome

検査に便利ですよね(everyとsome(any))

Array.prototype.every() - JavaScript | MDN

// ウォーミングアップ
cond = v => v > 2;
[1, 2, 3, 4].every(cond);
[10, 20, 30, 40].every(cond);
[1, 2, 3, 4].some(cond);

/* ユニークになっているか(Setは使わない方法) */
[10, 11, 12, 13].every((v, i, src) => src.indexOf(v) === i);

/* 後ろのものほど、大きくなっているか。*/
const nums = [10, 11, 12, 13];
nums.every((v, i, src) => src.slice(i + 1).findIndex(el => el < v) === -1);
nums.every((v, i, src) => src.slice(i + 1, i + 2).findIndex(el => el < v) === -1);

// undefinedを意識的に使って良いルールのもとでは、JavaScriptだともっとはっきり記述できる
nums.every((v, i, src) => src[i + 1] === undefined ? true : src[i + 1] > v);

findIndexも添字や元の配列を関数内で使ってfindできる

/* findIndexが思ったよりいろんなことができる...かも */
// 自身より3大きい数値が自身の前に2連続現れるものをfind(この例だと、13が該当するため、3が得られる。)
[10, 16, 16, 13, 14, 15, 16].findIndex((el, i, src) => {
    const x = el + 3;
    const pre = i - 2;
    return pre >= 0 ? src.slice(i - 2, i).every(x_ => x_ == x) : false;
});

自分の中ではヒット作なので、もう少し例がたまったらまたやるかも...

参考リンク

developer.mozilla.org

developer.mozilla.org

github.com

qiita.com