テスト駆動開発(TDD)の第三弾です。
環境
macOS BigSur メモリ16G Java 11.0.9.1 JUnit5 IntelliJ IDEA CE 2020.3
今回作成するもの
今回作成するものは、Javaを一通り学んだ(とは言ってもメソッドを学んだぐらいの段階ですが)大学生の私が深夜テンションで作った、西暦と月と日にちを与えればその日の曜日を計算できる機能です。
今回作るこの機能を、この記事ではモジュロ演算システムと呼ぶようにし、テスト駆動で開発してみることにします。
前回まで
前回は西暦の上2桁を取得するという非常にシンプルなテストだけを書いて終わりました。テストクラス(ModuloTest.java)と、実際のクラス(Modulo.java)は以下のようになっています。
ModuloTest.java
package modulo; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; public class ModuloTest { @Test public void testGetMethod(){ Modulo modulo = new Modulo(2021, 4, 1); assertEquals(20, modulo.getFirst2Digits()); assertEquals(21, modulo.getLast2Digits()); assertEquals(4, modulo.getMonth()); assertEquals(1, modulo.getDay()); } @Test public void testGetFirstAndLast2Digits(){ Modulo modulo = new Modulo(2021, 1, 1); assertEquals(20, modulo.getFirst2Digits()); assertEquals(20, modulo.getLast2Digits()); } @Test public void testGetFirstAndLast2Digits2(){ Modulo modulo = new Modulo(2000, 1, 1); assertEquals(19, modulo.getFirst2Digits()); assertEquals(99, modulo.getLast2Digits()); } @Test public void testGetMonth(){ Modulo modulo = new Modulo(2021, 2, 1); assertEquals(14, modulo.getMonth()); } }
Modulo.java
package modulo; public class Modulo { private int first2Digits; private int last2Digits; private int year; private int lastYear; private int newYear; private int month; private final int JANUARY = 13; private final int FEBRUARY = 14; private int day; public Modulo(int year, int month, int day) { this.first2Digits = 0; this.last2Digits = 0; this.year = year; this.lastYear = this.year - 1; this.newYear = 0; this.month = month; this.day = day; } public int getFirst2Digits(){ newYear = calcYearFromMonth(); first2Digits = newYear / 100; return first2Digits; } public int getLast2Digits(){ newYear = calcYearFromMonth(); last2Digits = newYear % 100; return last2Digits; } public int getMonth(){ month = calcMonth(); return month; } public int getDay(){ return day; } private int calcYearFromMonth(){ if(month == 1 || month == 2){ return lastYear; }else{ return year; } } private int calcMonth(){ if(month == 1){ return JANUARY; }else if(month == 2){ return FEBRUARY; }else{ return month; } } }
そして、現在のTODOリストは以下のようになっています。
TODOリスト
- 2021年4月1日を入力として与えると、木曜日を出力する
2021年の上二桁を取得2021年の下二桁を取得月を取得月が1月または2月だった場合は、それぞれ前年の13月、14月に変換月が1もしくは2の時に西暦を-1する月が1もしくは2の時に12を足す日にちを取得- チェラーの公式を使って曜日を出力する
現在取り組んでいるものは太字、終わったものに関しては取り消し線で表すことにします。
チェラーの公式を使って曜日を出力する
チェラーの公式を使って計算しましょう。例によってテストから。
ModuloTest.java
〜略〜 @Test public void testZeller(){ Modulo modulo = new Modulo(2021, 4, 1); assertEquals("木", modulo.calcZeller()); } 〜略〜
テストはこけるので、これを通すように作っていきます。
Modulo.java
〜略〜 public String calcZeller(){ return "木"; } 〜略〜
通りました。あとはこれをリファクタリングしていきます。
ここで、チェラーの公式は以下のようなものでした。ここでチェラーの公式で使われている文字と、実際のコードの変数との対応関係は以下になります。
文字 | 変数 |
---|---|
day | |
month | |
last2Digits | |
first2Digits |
そして、に対する数字と曜日の関係は以下になります。
曜日 | |
---|---|
0 | 土曜日 |
1 | 日曜日 |
2 | 月曜日 |
3 | 火曜日 |
4 | 水曜日 |
5 | 木曜日 |
6 | 金曜日 |
以上の情報を元に作っていきます。
Modulo.java
package modulo; import java.util.HashMap; import java.util.Map; public class Modulo { private int first2Digits; private int last2Digits; private int year; private int lastYear; private int newYear; private int month; private final int JANUARY = 13; private final int FEBRUARY = 14; private int day; private int h; //追記 public Modulo(int year, int month, int day) { this.first2Digits = 0; this.last2Digits = 0; this.h = 0;//追記 this.year = year; this.lastYear = this.year - 1; this.newYear = 0; this.month = month; this.day = day; } public int getFirst2Digits(){ newYear = calcYearFromMonth(); first2Digits = newYear / 100; return first2Digits; } public int getLast2Digits(){ newYear = calcYearFromMonth(); last2Digits = newYear % 100; return last2Digits; } public int getMonth(){ month = calcMonth(); return month; } public int getDay(){ return day; } public String calcZeller(){ h = (day + 26*(month+1)/10 + last2Digits + last2Digits/4 + first2Digits/4 + 5*first2Digits) % 7; // 追記 return "木"; } private int calcYearFromMonth(){ if(month == 1 || month == 2){ return lastYear; }else{ return year; } } private int calcMonth(){ if(month == 1){ return JANUARY; }else if(month == 2){ return FEBRUARY; }else{ return month; } } }
ここで、チェラーの公式で計算を行っているところで、- 2*first2Digits
と書くべきところを+ 5*first2Digits
と書いています。これは、多くのコンピュータの環境では負数の剰余を保証しません。(ってWikipediaに書いてありました。ちなみに、上記のチェラーの公式を使うのであれば、西暦1582年以降じゃないと無理っぽいですね。)
何を言っているかというと、余りを計算する場合、余りの値としては正の値が出てきて欲しいのですが、Javaに限らず他のプログラミング言語やコンピュータの環境では負の値を出してしまいます。例えば、
の余りは
という風に、余りの値を正として出して欲しいのですが、Javaだと
の余りは
と余りの値は負の値となってしまいます。チェラーの公式に当てはめるのであれば、正の値を出す必要があります。
そこで、整数の合同関係を使って計算することで、マイナスの値をプラスの値に変換することができます。
意外とこの合同関係って使うらしく、以下の動画でも使われています。6分あたりから合同式をうまく使って、テトリスの左回転を計算しています(なかなか面白いので、最初から見るのをお勧めします)。
最後にの数値を元に曜日を出します。ここはif文でもswitch文でもいいのですが、せっかくなので、Mapを使ってみましょう。の数値をkeyとして、曜日を返すようなMapです。
Modulo.java
package modulo; import java.util.HashMap; import java.util.Map; public class Modulo { private int first2Digits; private int last2Digits; private int year; private int lastYear; private int newYear; private int month; private final int JANUARY = 13; private final int FEBRUARY = 14; private int day; private int h; private Map<Integer, String> map; public Modulo(int year, int month, int day) { this.first2Digits = 0; this.last2Digits = 0; this.h = 0; this.year = year; this.lastYear = this.year - 1; this.newYear = 0; this.month = month; this.day = day; //追記 map = new HashMap<>(){ { put(0, "土"); put(1, "日"); put(2, "月"); put(3, "火"); put(4, "水"); put(5, "木"); put(6, "金"); } }; } public int getFirst2Digits(){ newYear = calcYearFromMonth(); first2Digits = newYear / 100; return first2Digits; } public int getLast2Digits(){ newYear = calcYearFromMonth(); last2Digits = newYear % 100; return last2Digits; } public int getMonth(){ month = calcMonth(); return month; } public int getDay(){ return day; } public String calcZeller(){ first2Digits = getFirst2Digits();//追記 last2Digits = getLast2Digits(); //追記 h = (day + (26*(month+1))/10 + last2Digits + last2Digits/4 + first2Digits/4 + 5*first2Digits) % 7; return map.get(h); //変更 } private int calcYearFromMonth(){ if(month == 1 || month == 2){ return lastYear; }else{ return year; } } private int calcMonth(){ if(month == 1){ return JANUARY; }else if(month == 2){ return FEBRUARY; }else{ return month; } } }
mapという変数を宣言しました(変数名のセンスの無さよ...)。これでをkeyとして曜日を取得することができます。
さて、これでテストを実行すると全て通ることが確認できるかと思います。
TODOリストを更新しましょう。ついでに、入力できる西暦も1582年〜というのも追加しておきましょう。
TODOリスト
2021年4月1日を入力として与えると、木曜日を出力する2021年の上二桁を取得2021年の下二桁を取得月を取得月が1月または2月だった場合は、それぞれ前年の13月、14月に変換月が1もしくは2の時に西暦を-1する月が1もしくは2の時に12を足す日にちを取得チェラーの公式を使って曜日を出力する- 西暦の入力を1582年以降に制限する
西暦の入力を1582年以降に制限する
上記で紹介しているチェラーの公式を使うには、1582年以降(グレゴリオ暦)出ないとちゃんと計算できません。ユリウス歴や紀元前も計算に含めればいい練習になるとは思うのですが、面倒になってきたのでやりません。
さて、例によってテストから。
ModuloTest.java
〜略〜 @Test public void testYearLimit(){ Modulo modulo = new Modulo(1581, 4, 1); assertEquals("西暦は1582年以降を入力してください", modulo.calcZeller()); } 〜略〜
テストは落ちるので、これを通すようにします。
Modulo.java
〜略〜 public String calcZeller(){ if(year < 1582){ return "西暦は1582年以降を入力してください"; }else{ first2Digits = getFirst2Digits(); last2Digits = getLast2Digits(); h = (day + (26*(month+1))/10 + last2Digits + last2Digits/4 + first2Digits/4 + 5*first2Digits) % 7; return map.get(h); } } 〜略〜
これでテストは通るようになりました。TODOリストを更新します。また、上記テストでは2021年の4月をテストしていますが、念の為、1月、2月の時もちゃんと出るか、また、西暦が2000年で月が1月、2月の場合とそうでない月の場合で正しい曜日が出るかをテストとして追加して確認してみましょう。
TODOリスト
2021年4月1日を入力として与えると、木曜日を出力する2021年の上二桁を取得2021年の下二桁を取得月を取得月が1月または2月だった場合は、それぞれ前年の13月、14月に変換月が1もしくは2の時に西暦を-1する月が1もしくは2の時に12を足す日にちを取得チェラーの公式を使って曜日を出力する西暦の入力を1582年以降に制限する- 2021年1月30日を入力として与えると、土曜日を出力する
- 2000年7月1日を入力として与えると、土曜日を出力する
- 2000年2月1日を入力として与えると、火曜日を出力する
テスト項目の追加
2021年1月30日を入力として与えると、土曜日を出力する
TODOリストをみやすくしましょう。すでに終わったタスクは消します。
TODOリスト
- 2021年1月30日を入力として与えると、土曜日を出力する
- 2000年7月1日を入力として与えると、土曜日を出力する
- 2000年2月1日を入力として与えると、火曜日を出力する
ModuloTest.java
〜略〜 @Test public void testZeller2(){ Modulo modulo = new Modulo(2021, 1, 30); assertEquals("土", modulo.calcZeller()); } 〜略〜
上記テストを追加しテストを実行すると、テストは通りませんでした。まじか...。ここまできてテストが通らないのは辛い...。
さて、原因を探ってみると、Modulo.java
のcalcZeller
メソッドの中身で使われている月がmonth
のままでした。さらに探るとgetMonth
メソッドがテストの時しか呼び出されておらず、さらにmonth
という変数に代入してしまっています。
ModuloTest.java
〜略〜 public int getMonth(){ month = calcMonth(); return month; } 〜略〜
ここで落ち着いて考えてみると、今のままだと、calcZeller
メソッドからgetMonth
メソッドを呼び、さらにgetMonth
メソッドからcalcMonth
メソッドを呼び出すことになってしまい、なんだか二度手間です。
それよりもcalcZeller
メソッドからcalcMonth
メソッドの処理を呼んで、newMonth
という新しい変数名に代入し、newMonth
をチェラーの公式の中で使うようにしたほうが良さそうです。
つまり、
Modulo.java
package modulo; import java.util.HashMap; import java.util.Map; public class Modulo { private int first2Digits; private int last2Digits; private int year; private int lastYear; private int newYear; private int month; private int newMonth;//追記 private final int JANUARY = 13; private final int FEBRUARY = 14; private int day; private int h; private Map<Integer, String> map; public Modulo(int year, int month, int day) { this.first2Digits = 0; this.last2Digits = 0; this.h = 0; this.year = year; this.lastYear = this.year - 1; this.newYear = 0; this.month = month; this.newMonth = 0; //追記 this.day = day; map = new HashMap<>(){ { put(0, "土"); put(1, "日"); put(2, "月"); put(3, "火"); put(4, "水"); put(5, "木"); put(6, "金"); } }; } public int getFirst2Digits(){ newYear = calcYearFromMonth(); first2Digits = newYear / 100; return first2Digits; } public int getLast2Digits(){ newYear = calcYearFromMonth(); last2Digits = newYear % 100; return last2Digits; } public int getMonth(){//中身の処理を変更 if(month == 1){ return JANUARY; }else if(month == 2){ return FEBRUARY; }else{ return month; } } public int getDay(){ return day; } public String calcZeller(){ if(year < 1582){ return "西暦は1582年以降を入力してください"; }else{ first2Digits = getFirst2Digits(); last2Digits = getLast2Digits(); newMonth = getMonth(); //追記 h = (day + (26*(newMonth+1))/10 + last2Digits + last2Digits/4 + first2Digits/4 + 5*first2Digits) % 7; return map.get(h); } } private int calcYearFromMonth(){ if(month == 1 || month == 2){ return lastYear; }else{ return year; } } //使わない private int calcMonth(){ if(month == 1){ return JANUARY; }else if(month == 2){ return FEBRUARY; }else{ return month; } } }
こんな感じです。これでテストを走らせると全てグリーンになりました。あとはModulo.javaでcalcMonth
メソッドは使わなくなったので、それを消してリファクタリング終了です。TODOリストも更新します。
TODOリスト
2021年1月30日を入力として与えると、土曜日を出力する- 2000年7月1日を入力として与えると、土曜日を出力する
- 2000年2月1日を入力として与えると、火曜日を出力する
2000年7月1日を入力として与えると、土曜日を出力する
次のTODOリストに残っているタスクを確認し、取り組みましょう。
TODOリスト
2021年1月30日を入力として与えると、土曜日を出力する- 2000年7月1日を入力として与えると、土曜日を出力する
- 2000年2月1日を入力として与えると、火曜日を出力する
ModuloTest.java
〜略〜 @Test public void testZeller3(){ Modulo modulo = new Modulo(2000, 7, 1); assertEquals("土", modulo.calcZeller()); } 〜略〜
テストを走らせると、全て通ることを確認できます。他に特にやることはないので、TODOリストを更新して終了です。
TODOリスト
2021年1月30日を入力として与えると、土曜日を出力する2000年7月1日を入力として与えると、土曜日を出力する- 2000年2月1日を入力として与えると、火曜日を出力する
2000年2月1日を入力として与えると、火曜日を出力する
TODOリスト
2021年1月30日を入力として与えると、土曜日を出力する2000年7月1日を入力として与えると、土曜日を出力する- 2000年2月1日を入力として与えると、火曜日を出力する
ModuloTest.java
〜略〜 @Test public void testZeller4(){ Modulo modulo = new Modulo(2000, 2, 1); assertEquals("火", modulo.calcZeller()); } 〜略〜
テストを走らせると、全て通ることを確認できます。他に特にやることはないので、TODOリストを更新して終了です。
TODOリスト
2021年1月30日を入力として与えると、土曜日を出力する2000年7月1日を入力として与えると、土曜日を出力する2000年2月1日を入力として与えると、火曜日を出力する
これでTODOリストのタスク全てを消化できました。これで作りたかったモジュロ演算システムは完成です。お疲れ様でした。
TDDを一通りやってみて
最後はテストが通らないという事件もありましたが、無事最後までやり遂げることができました。途中、私が疲れてしまったところもあり雑に書いてしまった部分もあるかと思います。
とはいえ、ある程度TDDの一通りの手順は理解いただけたのでは無いでしょうか?
私自身、実際にTDDで開発してみて、変数名やメソッド名の付け方、クラスを分けるなど、どの部分がスキル的に足りないかというのを見つめ直すいい機会になりました。足りない部分に関してはまた一から勉強していければと考えています。
今回、TDDでの開発を3回に渡って書き記してきましたが、ここで書いた内容はTDD全体の一部でしかありません。本気でTDDを学びたいのであれば、書籍や他の記事を参考にしつつ、ご自身の手で書いていくことが一番身につきやすいと思います。
また、TDD全てを真似する必要はありませんし、人それぞれ向き不向きがあるかと思います。TDDや他の開発手法のいいとこ取りをしつつ、自分なりの開発手法を身につけていけばいいの
ではないでしょうか?
今回の記事が皆さんのお役に立てれば幸いです。
ソースコード
Modulo.java
package modulo; import java.util.HashMap; import java.util.Map; public class Modulo { private int first2Digits; private int last2Digits; private int year; private int lastYear; private int newYear; private int month; private int newMonth; private final int JANUARY = 13; private final int FEBRUARY = 14; private int day; private int h; private Map<Integer, String> map; public Modulo(int year, int month, int day) { this.first2Digits = 0; this.last2Digits = 0; this.h = 0; this.year = year; this.lastYear = this.year - 1; this.newYear = 0; this.month = month; this.newMonth = 0; this.day = day; map = new HashMap<>(){ { put(0, "土"); put(1, "日"); put(2, "月"); put(3, "火"); put(4, "水"); put(5, "木"); put(6, "金"); } }; } public int getFirst2Digits(){ newYear = calcYearFromMonth(); first2Digits = newYear / 100; return first2Digits; } public int getLast2Digits(){ newYear = calcYearFromMonth(); last2Digits = newYear % 100; return last2Digits; } public int getMonth(){ if(month == 1){ return JANUARY; }else if(month == 2){ return FEBRUARY; }else{ return month; } } public int getDay(){ return day; } public String calcZeller(){ if(year < 1582){ return "西暦は1582年以降を入力してください"; }else{ first2Digits = getFirst2Digits(); last2Digits = getLast2Digits(); newMonth = getMonth(); h = (day + (26*(newMonth+1))/10 + last2Digits + last2Digits/4 + first2Digits/4 + 5*first2Digits) % 7; return map.get(h); } } private int calcYearFromMonth(){ if(month == 1 || month == 2){ return lastYear; }else{ return year; } } }
ModuloTest.java
package modulo; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; public class ModuloTest { @Test public void testGetMethod(){ Modulo modulo = new Modulo(2021, 4, 1); assertEquals(20, modulo.getFirst2Digits()); assertEquals(21, modulo.getLast2Digits()); assertEquals(4, modulo.getMonth()); assertEquals(1, modulo.getDay()); } @Test public void testGetFirstAndLast2Digits(){ Modulo modulo = new Modulo(2021, 1, 1); assertEquals(20, modulo.getFirst2Digits()); assertEquals(20, modulo.getLast2Digits()); } @Test public void testGetFirstAndLast2Digits2(){ Modulo modulo = new Modulo(2000, 1, 1); assertEquals(19, modulo.getFirst2Digits()); assertEquals(99, modulo.getLast2Digits()); } @Test public void testGetMonth(){ Modulo modulo = new Modulo(2021, 2, 1); assertEquals(14, modulo.getMonth()); } @Test public void testZeller(){ Modulo modulo = new Modulo(2021, 4, 1); assertEquals("木", modulo.calcZeller()); } @Test public void testYearLimit(){ Modulo modulo = new Modulo(1581, 4, 1); assertEquals("西暦は1582年以降を入力してください", modulo.calcZeller()); } @Test public void testZeller2(){ Modulo modulo = new Modulo(2021, 1, 30); assertEquals("土", modulo.calcZeller()); } @Test public void testZeller3(){ Modulo modulo = new Modulo(2000, 7, 1); assertEquals("土", modulo.calcZeller()); } @Test public void testZeller4(){ Modulo modulo = new Modulo(2000, 2, 1); assertEquals("火", modulo.calcZeller()); } }