この記事はニフティグループ Advent Calendar 2020 の3日目の記事です。
昨日は @hmmrjn さんで「Webエンジニアが入社2年目までに習得したコマンド」でした。まだまだ知らない便利コマンドがたくさんあり、自分も使いこなせるようになる日が待ち遠しいです……!
はじめに
私の所属しているチームでは、会議ごとに毎回司会と議事録係をメンバー内でローテーションしています。ある会議ではAさんが司会でBさんが議事録、次の週の会議ではBさんが司会でCさんが議事録……という感じです。
一応順番も決まっているのですが、人間意外とこの順番を忘れてしまいます。これを忘れないようにするため、司会と議事録を自動で書き出すReactアプリを作成したのですが、今回はその時に__あー、こうしておけばもっとスマートに実装できたなー__という、後から思いついたことを記録しておこうと思います。
やろうとしたこと
週ごとに司会と議事録のメンバーがローテーションするため、for文やらmap関数あたりでぐるぐる回した際に、いい感じに週ごとの司会と議事録係を吐き出してUIコンポーネントに渡せるようにしようとしました。
まず、前提として全メンバーが担当順に並んだリストがあります。
const memberList = ["A", "B", "C", "D", "E", "F"];
これを最初のループでは"A", "B"
、次は"B", "C"
、その次は"C", "D"
という感じで指定できるように回せれば良いわけです。
唯一の注意点としては、Fさんまで行ったらその次はAさんに戻ることですね。
方法
割と簡単なお題ですが、実装方法としてはいくつか考えられます。
悩まないパターン:「ひゃっはーリストを連結だー!」
「Fさんまで行ったらその次はAさんに戻る」なら、Fさんの後にAさんが来るリストがあれば解決ですね!!!
const newMemberList = memberList.concat(memberList).concat(memberList);
let [i, j] = [0, 1];
for (let k = 0; k < 10; k++) {
const [moderator, minutes] = [newMemberList[i], newMemberList[j]];
console.log(moderator, minutes);
[i, j] = [i + 1, j + 1];
}
A B
B C
C D
D E
E F
F A
A B
B C
C D
D E
この方法の問題点は言わずもがな、最初に連結した分の配列を超えた瞬間、司会と議事録はundefinedになります。残念ながら開発メンバーにundefinedさんはいないので、会議は人だけが集まり何も話されず何も記録されず、やがては会議の内容自体がundefinedだったことになるでしょう……。
たぶんこれを書いたときは疲れてたんだと思います。そうだと信じたい。(これを書いた数日後に改修しました。が、それでもなんか無駄に複雑なことしてます……)
単純なパターン:「配列の添え字を配列の添え字で割りつつ、ぐるぐる回す」
気を取り直して一番単純なパターンでいきましょう。
let [i, j] = [0, 1];
for (let k = 0; k < 10; k++) {
const [moderator, minutes] = [memberList[i], memberList[j]];
console.log(moderator, minutes);
[i, j] = [i, j].map((l) => (l + 1) % memberList.length);
}
A B
B C
C D
D E
E F
F A
A B
B C
C D
D E
ループ内で添え字をインクリメントしつつ、memberListの長さで割った余りを代入することでシンプルにローテーションしています。これなら突如として10年後の司会と議事録まで一気に表示することになっても大丈夫そうです。
また、もしかするとローテーションを使いまわすかもしれないので、関数化してしまいましょう。
const createRotationList = (list, length) => {
let [i, j] = [0, 1];
const rotationList = [];
for (let l = 0; l < length; l++) {
const [moderator, minutes] = [list[i], list[j]];
rotationList.push([moderator, minutes]);
[i, j] = [i, j].map((l) => (l + 1) % list.length);
}
return rotationList;
};
>console.log(createRotationList(memberList,10))
(10) [Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2)]
0: (2) ["A", "B"]
1: (2) ["B", "C"]
2: (2) ["C", "D"]
3: (2) ["D", "E"]
4: (2) ["E", "F"]
5: (2) ["F", "A"]
6: (2) ["A", "B"]
7: (2) ["B", "C"]
8: (2) ["C", "D"]
9: (2) ["D", "E"]
悪くないですね。でも、せっかくなのでもっと汎用的にしてみましょう。もしかすると、今後司会・議事録のほかに賑やかし係や会議BGM係がローテーションの中に追加されるかもしれません。
便利な関数にしてみたパターン:「オプションましまし全部盛りで」
関数内で決めていた値を、オプションとして入力できる引数にすることで、いろいろと指定できるようにしましょう。
const createRotationList = (list, length, opt = { resItemNum: 1, step: 1, startIndex: 0 }) => {
// 配列のどこからカウントするか
const startIndex = opt.startIndex ? opt.startIndex : 0;
// どの程度進めるか
const step = opt.step ? opt.step : 1;
// いくつの要素を出力するか
const resItemNum = opt.resItemNum ? opt.resItemNum : 1;
let index = [...Array(resItemNum).keys()].map((i) => (i + startIndex) % list.length);
const rotationList = [];
for (let l = 0; l < length; l++) {
const resItems = index.map((i) => list[i]);
rotationList.push(resItems);
index = index.map((i) => (i + step) % list.length);
}
return rotationList;
};
>createRotationList(memberList,10,{resItemNum:2})
(10) [Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2)]
0: (2) ["A", "B"]
1: (2) ["B", "C"]
2: (2) ["C", "D"]
3: (2) ["D", "E"]
4: (2) ["E", "F"]
5: (2) ["F", "A"]
6: (2) ["A", "B"]
7: (2) ["B", "C"]
8: (2) ["C", "D"]
9: (2) ["D", "E"]
若干機能が過剰なような気もしますが、これでメンバーをローテーションさせ放題……と思いきや、リストを吐き出す関数にさせてしまったせいで、最初にローテーションさせる回数を指定しないといけなくなってるのがちょっと嫌ですね。っていうかこれ、長さを指定してるだけで最初にリスト連結させまくったのと同じじゃん!
まあカレンダーとかに表示するのであれば、大抵の場合表示回数とか事前にわかるような気もしますが。
クロージャを使ってみるパターン:「関数を実行すると担当者が出るよ!」
みなさんはJavaScriptのクロージャという機能をご存じでしょうか。自分は一応聞いたことがある程度で、いままで実際に使ったことはありませんでした。
じゃあせっかくだから使ってみよう!
const createListRotation = (list, opt = { resItemNum: 1, step: 1, startIndex: 0 }) => {
const startIndex = opt.startIndex ? opt.startIndex : 0;
const step = opt.step ? opt.step : 1;
const resItemNum = opt.resItemNum ? opt.resItemNum : 1;
let index = [...Array(resItemNum).keys()].map((i) => (i + startIndex) % list.length);
const listRotation = () => {
const resItems = index.map((i) => list[i]);
index = index.map((i) => (i + step) % list.length);
return resItems;
};
return listRotation;
};
>const nextMember = createListRotation(memberList, { resItemNum:2 });
>nextMember();
["A", "B"]
>nextMember();
["B", "C"]
>nextMember();
["C", "D"]
ここで作成した関数についてですが、構造はほぼ先ほど作成した関数と変わりません。違っているのは、関数の返り値としてローテーションごとのメンバーリストが得られるのではなく、「実行するたびに次のタイミングのローテーションのメンバーが出力される関数」が得られる点です。これをfor文やmap関数の中で実行すると、そのループの担当メンバーがわかります。
初めから必要なローテーション回数を数える必要もなく、これなら何回会議があっても担当者がわかりますね!
クラスを使ってみるパターン:「ES6ならクロージャじゃなくてクラスでも良くないですか?」
ES6ならJavaScriptにもクラスがあります。ということで、クロージャで実装したものをクラスに変えてみましょう。
class RotationList {
constructor(list, opt = { resItemNum: 1, step: 1, startIndex: 0 }) {
this.list = list.concat();
this.startIndex = opt.startIndex ? opt.startIndex : 0;
this.step = opt.step ? opt.step : 1;
this.resItemNum = opt.resItemNum ? opt.resItemNum : 1;
this.index = [...Array(this.resItemNum).keys()].map((i) => (i + this.startIndex) % this.list.length);
}
nextRotation() {
const resItems = this.index.map((i) => this.list[i]);
this.index = this.index.map((i) => (i + this.step) % this.list.length);
return resItems;
}
}
>const memberRotation = new RotationList(memberList, { resItemNum:2 })
>memberRotation.nextRotation()
["A", "B"]
>memberRotation.nextRotation()
["B", "C"]
こちらも良さそうです。何やらクロージャはメモリリークしたりする可能性もあるらしく、ES6が使える環境ならこっちの方が良いのかもしれませんね。ただ、クロージャに比べるとコンストラクタを書くのが若干面倒な気もしますが。
おわりに
というわけで、実はただクロージャが使ってみたかっただけの記事でした。あとからクラスの存在を思い出し、軽く調べたら「今ならクラスの方が良い……?それならクラスも併記しておかないとマズイ?」となった結果思ったよりも時間を取られてしまいました。
それでも初めて見たとき、クロージャってこれ何に使うんだとまあ頭をひねったわけですが、案外使い道が出てくるものですね。
これからはもっと良い実装をしたいものです。
次回予告
さて、本日は何とか12/3中に記事を上げられたわけですが、なんと今日に引き続き明日も記事は__現在未定__です!
なんとかなれ!