CodeKataをKotlinでやってみた 〜Counting Code Lines編〜

今回も[CodeKata](http://codekata.com/)をKotlinでやっていきたいと思います。
「そもそもCodeKataって何？」と言う方は"[CodeKataをKotlinでやってみた 〜Karate Chop編〜](https://qiita.com/Takuyaaaa/items/1d47fb67a43fb4f3c919)"をご参照ください。

## トライしてみる
今回の課題は21あるKataのうち13つ目に当たる"[Counting Code Lines](http://codekata.com/kata/kata13-counting-code-lines/)"です。

>This week let’s write something vaguely useful: a utility that counts the number of lines of actual code in a Java source file. For the purpose of this exercise, a line is counted if it contains something other than whitespace or text in a comment. Some simple examples:
>
```java
  // This file contains 3 lines of code
  public interface Dave {
    /**
     * count the number of lines in a file
     */
    int countLines(File inFile); // not the real signature!
   }
``` 
> and…
>
```java
/*****
* This is a test program with 5 lines of code
*  \/* no nesting allowed!
//*****//***/// Slightly pathological comment ending...
>
public class Hello {
    public static final void main(String [] args) { // gotta love Java
      // Say hello
      System./*wait*/out./*for*/println/*it*/("Hello/*");
    }
> 
}
```

つまり、上記で提示されている2つのJavaコードで実際にコードが記述されている行数（空行やコメントのみが記述されている行は含まない）をカウントするプログラムを実装することが今回の課題になります。今回はKotlinで実装を進めていくので、カウント対象コードについてもJavaではなく一部修正してKotlinとして取り扱うことにします。

### 実装

```kotlin
class CountingCodeLines {
    companion object {
        /**
         * count the number of lines of actual code; i.e. not counting comment lines
         */
        fun File.countCodeLines(): Int {
            return readLines()
                    .filter { it.isNotBlank() }
                    // need to deal with multiple lines comment first as
                    // "//" might be used as first character of multiple lines comment
                    .excludeMultipleLinesComment()
                    .filter { it.isNotOneLineComment() }
                    .count()
        }

        /**
         * exclude multiple lines comment not to count
         */
        private fun List<String>.excludeMultipleLinesComment(): List<String> {
            var inMultipleLinesComments = false
            return this.filter {
                when {
                    // when it starts, flag turns into true and the line would be excluded
                    it.multipleLinesCommentStarts(inMultipleLinesComments) -> {
                        inMultipleLinesComments = true
                        return@filter it.beforeCodeExists()
                    }

                    // when it ends, flag turns into false and the line would be excluded
                    // if there is no actual code after ending of multiple lines comment
                    it.multipleLinesCommentEnds(inMultipleLinesComments) -> {
                        inMultipleLinesComments = false
                        return@filter it.afterCodeExists()
                    }

                    // when the line is not in multiple lines comment, the line would be passed to count
                    else -> return@filter !inMultipleLinesComments
                }
            }
        }

        /**
         * if multiple lines comment starts
         */
        private fun String.multipleLinesCommentStarts(inMultipleLinesComments: Boolean): Boolean =
                this.contains("/**") && !inMultipleLinesComments

        /**
         * if multiple lines comment ends
         */
        private fun String.multipleLinesCommentEnds(inMultipleLinesComments: Boolean): Boolean =
                this.contains("*/") && inMultipleLinesComments

        /**
         * if there is no actual code before starting of multiple lines comment
         */
        private fun String.beforeCodeExists(): Boolean  {
            val beforeCode = this.split("/**")[0]
            return beforeCode.isNotBlank() && beforeCode.isNotOneLineComment()
        }

        /**
         * if there is no actual code after ending of multiple lines comment
         */
        private fun String.afterCodeExists(): Boolean  {
            val afterCode = this.split("*/").last()
            return afterCode.isNotBlank() && afterCode.isNotOneLineComment()
        }

        /**
         * if the line is one line comment
         */
        private fun String.isNotOneLineComment(): Boolean  {
            return  this.split("//")[0].isNotBlank() 
                    && this.split("/*")[0].isNotBlank()
                    && this.split("*/").last().isNotBlank()
        }
    }
}
```

読み込んだFileに対して`countCodeLines`メソッドを呼び出すことで実際にコードが記述されている行数をカウントすることができます。"**空行を削除** -> `/** ~ */`**として記述される複数行コメントを削除** -> `//`、`/* ~ */`**として記述される一行コメントを削除** -> **残存行数をカウント**"の流れで処理を実行していきます。

空行については単純に`filter { it.isNotBlank() }`として削除していきます。

次に複数行コメントを削除していくのですが、こちらは`excludeMultipleLinesComment`メソッドで処理を行います。詳細は実装をご覧いただければと思いますが、主たるアイディアとしては「複数行コメント開始の合図`/**`が出現したら、複数行コメント終了の合図`*/`が出現するまでその中間行は削除対象とする」というものです。
`/**`が出現した場合には`inMultipleLinesComments`をtrueとすることで`*/`が出現するまでの行をスキップします。また、`/**`が記述されていたとしても、その前にコードが記述されている場合にはカウント対象とする必要があるため、`multipleLinesCommentStarts`で前に記述が存在するかを確認します。`*/`が出現した場合も同様で、`afterCodeExists`で後に記述が存在するかを確認します。

最後に一行のコメントを`filter { it.isNotOneLineComment() }`として削除していきます。`isNotOneLineComment`はStringの拡張メソッドです。`//`と`/* ~ */`として記述される一行コメントを削除していきますが、その際に複数行の場合と同様に前後（`//`は前だけチェック）に通常の記述が存在する場合にはカウント対象としています。

### テストケース整備
それではテストを整備し意図通りの挙動となっているかを確認していきます。テストケースは3つ用意していて、1つ目と2つ目は冒頭で紹介した課題作成者提供のコードを流用します。最後の1つに関しては上記で示した実装コード自体をテスト対象として読み込んでいます。

```kotlin
class CountingCodeLinesTest {
    @Test
    fun testCount1() {
        val count = File("resources/CountTarget1.kt")
                .countCodeLines()
        assertEquals(3, count)
    }

    @Test
    fun testCount2() {
        val count = File("resources/CountTarget2.kt")
                .countCodeLines()
        assertEquals(5, count)
    }

    @Test
    fun testCount3() {
        val count = File("src/code_kata/CountingCodeLines.kt")
                .countCodeLines()
        assertEquals(44, count)
    }
}
```

無事にテストケースを通過し、プログラムが意図通り動いていることが確認できました！

## まとめ
今回の課題はこれまでのCodeKataの中で最も難易度が高いと感じました。自身の実装も少し読みづらいと感じる部分もあり、他のKataと比べても実装に改善の余地ありだなと正直感じています。
また、自分の手元で様々なケースを検証したとはいえ、今回作成したプログラムが一寸のバグも無くKotlinコードのカウントを行えることは保証致しかねますのでご了承ください。

お読みいただきまして、ありがとうございました！

**関連記事一覧**

* [CodeKataをKotlinでやってみた 〜Karate Chop編〜](https://qiita.com/Takuyaaaa/items/1d47fb67a43fb4f3c919)
* [CodeKataをKotlinでやってみた 〜Data Munging編〜](https://qiita.com/Takuyaaaa/items/a6b97ba6b1864451e771)
* [CodeKataをKotlinでやってみた 〜Bloom Filters編〜]
(https://qiita.com/Takuyaaaa/items/eaa3848bce3bccdd946f)
* [CodeKataをKotlinでやってみた 〜Anagrams編〜]
(https://qiita.com/Takuyaaaa/items/df06e24e6f2c7f8ced35)
* [CodeKataをKotlinでやってみた 〜Checkout編〜]
(https://qiita.com/Takuyaaaa/items/0a4b82e30c977444c0bc)
* [CodeKataをKotlinでやってみた 〜Sorting it Out編〜]
(https://qiita.com/Takuyaaaa/items/b5210c53bff3ff5f0512)
* [CodeKataをKotlinでやってみた 〜Counting Code Lines編〜]
(https://qiita.com/Takuyaaaa/items/cb9143fbcb9e0b2a7822)
* [CodeKataをKotlinでやってみた 〜Tom Swift Under the Milkwood編〜]
(https://qiita.com/Takuyaaaa/items/feccf69d2b9d95196a72)
* [CodeKataをKotlinでやってみた 〜Transitive Dependencies編〜]
(https://qiita.com/Takuyaaaa/items/9b43473b8feffe1ce9f7)
* [CodeKataをKotlinでやってみた 〜Word Chains編〜]
(https://qiita.com/Takuyaaaa/items/2539338252ad7e19ba18)
* [CodeKataをKotlinでやってみた 〜Simple Lists編〜]
(https://qiita.com/Takuyaaaa/items/36ef73522bfe8d054448)