四半期計算から考えるコードの書き方


はじめに

今、簡単な新人育成のためのカリキュラムを作っています。
なので 初心者向けの内容になっています。
熟練者の方は その教育間違っているかも ってあれば突っ込んでください!!

この記事では "超簡単な問題" から、あるべきコードの 正しい書き方とは何かを教えたい記事です。

問題1

Q.

入力として1月から12月を、それが第何四半期かを返す関数(calcQ)を作りなさい。
1-3月は1、4-6月は2、7-9月は3、10-12月は4です

A.

calcQ.js
function calcQ (month) {
  return parseInt((month-1) / 3, 10) + 1;
}

考察

それほど難しくないですね。簡単!!
1から12を3で割って計算するだけです。
これくらいは、動かさなくても間違っていないだろう。。。って?
でも 必ずテストコードをきちんと書きましょう

テストコードの記述に決まりはありませんが、node.jsのassertを使うと次のとおり

testCalcQ.js
var eq = require('assert').equal;
eq(calcQ(1), 1);
eq(calcQ(2), 1);
 // (中略)
eq(calcQ(11), 4);
eq(calcQ(12), 4);

テストコードで注意することは、次のとおりです。

  • 必ずテストコードを書くこと
  • for文など使わないで、ベタベタなコードを書くこと
    • テストコードにロジックを入れると、テストコードのテストコードが必要になります
  • 考えられる入力を網羅すること(無限にある場合は、閾値を網羅すること)
  • まったく入力される可能性のない値の入力のテストコードは書かないこと(余分です)

一度テストコードを書く癖をつけることで、のちのち不具合の箇所を素早く見つけることができるようになります

それと同時にコメントも 必ず記述してください
コメントの形式は、JsDocを標準としましょう。

calcQ.js
/**
 * 四半期を返す
 *  1-3月は1、4-6月は2、7-9月は3、10-12月は4です
 * @method calcQ
 * @param  {Number} month   月
 * @return {Number}         四半期
 */
function calcQ(month) {
  return parseInt((month-1) / 3, 10) + 1;
}

コメントの記述で注意することは、次のとおりです

  • ソースコードの中にコメントを詳しく書く必要はありません。(だれもそこは求めていません)
  • 関数の前に記述するコメントは何をする関数なのかをできるだけパッと理解できるように書きましよう。
  • 指示書が存在する場合は、なにも考えずにコピペして貼り付けると楽です
    • 今回の場合はQの部分ですね
  • 処理内容を書いてはいけません。(月数を3で割って1を足していますとか)
    • 処理内容はソースコードを見ればだいたいわかります。
  • コメントの記述は自動的にテンプレートを挿入してくれるプラグインをIDEに設定するべきです。
    • 例えばSublimeTextにはDocBlockrが存在します

問題2

Q.

日本での四半期を返す関数(calcQJ)を作りなさい。
1-3月は4、4-6月は1、7-9月は2、10-12月は3です

考え中...

えっと、1から3と4以上を別けて考えよう。
3以下は4になるからif文で分岐して、それ以外は3を引けばcalcQの時と同じだな。

calcQJ.js
/**
 * (日本の)四半期を返す
 *  1-3月は4、4-6月は1、7-9月は2、10-12月は3です
 * @method calcQJ
 * @param  {Number} month 月
 * @return {Number}       四半期
 */
function calcQJ(month) {
  if (month <= 3) {
    return 4;
  }
  month = month - 3;
  return parseInt((month-1) / 3, 10) + 1;
}

テストもOKだ

testCalcQJ.js
var eq = require('assert').equal;
eq(calcQ(1), 4);
eq(calcQ(2), 4);
 // (中略)
eq(calcQ(11), 3);
eq(calcQ(12), 3);

A

正しいことは正しいですが、処理の部分の理想は次のとおりです。
(コメント・テストは省略していますが、本来は記述してね)

calcQJ.js
function calcQJ(month) {
  return calcQ(month) - 1 || 4;
}

まず、後半部分のparseInt((month-1) / 3, 10) + 1はコピペするくらいなら定義済みの関数を使用しましょう。
それと、calcQJとcalcQには結果にある程度の規則があることに気がつきます。
1を引けば、四半期が1つずれることに注目しましょう。
せっかくなのでそのまま利用します。
ただし、それだと1-3月が0になります。if文で結果を分けてもいいですが。。。
javascriptには||を使うと簡単にfalsyな値(0, '', false, null等)の代わりの値を使うことができるようなイディオムが存在します

考察

ここまで、短いコードで問題2が書けることは、ちょっと驚きますよね。
なかなかそこまで到達するのは大変ですが、べたべたな記述から少し頭を使うことでエレガントなコードになります

良質なイディオムは積極的につかうべきですが、あまり複雑なものは避けましょう。
良質なイディオムとは、オープンソースなど他人のコードでは、よく使われているので感覚的にわかるようになります。
よく見るものは、すぐに理解できるようになりましょう。

問題3

Q.

四半期の開始月を自由に設定できる関数(calcQEx)を作りなさい。
入力は第一引数に月、第二引数に開始月を指定します
例) 開始月が9月の時に1月は第2四半期です

考え中...

さっきのような規則性は簡単に思いつかないなーー(考えるの面倒だし)
ベタだけど、開始月より小さい月は12を足して、開始月が1になるようにずらせばいいんじゃない?
こんな感じかな?
今度は、定義済みのcalcQを使ったよ!

calcQEx.js
function calcQEx (month, start) {
  if (month < start) {
    month = month + 12;
  }
  month = month - start + 1;
  return calcQ(month);
}

A.

よくできました!
でも、すこしリファクタリングしましょうか。
ベタに書かれたソースコードもリファクタリングすると、できる人のコードっぽくなります。
monthの再代入をしているので、monthの値が行ごとに変更されていてちょっと混乱しますねー
あまり行儀がいいとは言えません
入力の値はそのままにしたほうがよいでしょう

calcQEx.js
function calcQEx (month, start) {
  var index;
  if (month < start) {
    index = month + 12;
  } else {
    index = month;
  }
  index = index - start + 1;
  return calcQ(index);
}

if文はよく使われる三項演算子で書き換えられそうです
三項演算子の使用は議論が分かれることがありますが、よく見るのでなれましょう

calcQEx.js
function calcQEx (month, start) {
  var index = month < start ? month + 12 : month;
  index = index - start + 1;
  return calcQ(index);
}

式をすこし整理すると、次のようになります
わかりにくいコードになったなーと思いますね。
でもテストをきちんと書いていれば、それでも大丈夫です。
ただ、自分や他人が後でコードを見た時にこの行なんだっけ?っとならないためのコメントを入れるのは推奨します

calcQEx.js
function calcQEx (month, start) {
  // startを仮想1月としたら何月になるか
  var index = month - start + (month < start ? 13 : 1);
  return calcQ(index);
}

これで実際の処理を詳しく見なくても、indexってなにを表すんだろうとわかります

考察

さてこれで完成! ...でもいいんですが
イディオムを使った場合は同じ処理が次のようにかけちゃいます

calcQEx.js
function calcQEx (month, start) {
  // startを仮想1月としたら何月になるか
  var index = (month - start + 13) % 12 || 12;
  return calcQ(index);
}

三項演算子が消えて%演算子と||を使ってますね
解説はしませんが、なぜ同じ処理になるのか考えてみてくださいね。