読者です 読者をやめる 読者になる 読者になる

Scalaで「フルパスから相対パスを求める」

懲りずにやってみた。
お題はこちら。

お題:フルパスから相対パスを求める - No Programming, No Life

お題とは直接関係ないけど、例外のテストを行うための@Test(expected=classOf[HogeHoge]) をうまく略記する方法はないものでしょうか。

※仕様が追加されていたので修正しました

object RelativePath {
    
    val sepChar = '/'

    def getRelativePath(base: String, target: String) : String = {

        // 不正文字チェック
        def check(p: String) = {
            require(p != null, "Parameter must not be null")
            require(p.nonEmpty, "Parameter must not be empty")
            require(""".*[\\\?\*\:\"\|\>\<].*|.*\.$""".r.findFirstIn(p) == None, "Bad character")
            require(p.startsWith(sepChar.toString()), "Parameter must start with Slash")
        }
        check(base); check(target)

        // 末端ノードの整理
        val l1 = (base.split(sepChar).filter(_.nonEmpty).toList, base.last) match {
            case (x, `sepChar`) => x
            case (x, _) => x.init
        }
        val l2 = (target.split(sepChar).filter(_.nonEmpty) ++ 
            Array(target.last).collect{case `sepChar` => ""}).toList
        
        // 共通する上位パスを除去(分岐ノードまで降りてくる)
        def removeSame(l1: List[String], l2:List[String]): (List[String], List[String]) = l1 match {
            case hl1::tl1 if (hl1 == l2.head) => removeSame(tl1, l2.tail)
            case _ => (l1, l2)
        }
        removeSame(l1, l2) match {
            // 同レベルディレクトリ
            case (Nil, sl2) => sl2.mkString("./",sepChar.toString,"")
            // 分岐ノードまでの段数分「../」の生成
            case (sl1, sl2) => (sl1.map(_ => "..") ++ sl2).mkString("",sepChar.toString,"")
        }
    }
}

/** 以下テスト */
class RelativePathTest {
    import org.junit.Assert._
    import org.junit.Test

    import RelativePath._
    
    @Test
    def testGetRelativePath = {
        def as(p1: String, p2: String, to: String) = assertEquals(to, getRelativePath(p1, p2))
        
        as("/aaa/bbb/from.txt", "/aaa/bbb/to.txt",      "./to.txt")
        as("/aaa/bbb/from.txt", "/aaa/to.txt",          "../to.txt")
        as("/aaa/bbb/from.txt", "/aaa/bbb/ccc/to.txt",  "./ccc/to.txt")
        as("/aaa/bbb/from.txt", "/aaa/ccc/ddd/to.txt",  "../ccc/ddd/to.txt")
        as("/aaa/bbb/from.txt", "/ddd/ccc/to.txt",      "../../ddd/ccc/to.txt")
        as("/aaa/bbb/",         "/aaa/ddd/to",          "../ddd/to")
        as("/aaa/bbb/",         "/aaa/ccc/",            "../ccc/")
        as("/aaa/bbb/ccc.txt",  "/aaa/bbb/ccc.txt",     "./ccc.txt")        
        as("/aaa/",             "/aaa/",                "./")
        as("//aaa///bbb////from.txt", "/////aaa//////bbb///////to.txt", "./to.txt")
        as("//aaa///bbb///fff//from.txt", "/////aaa//////bbb///////to.txt", "../to.txt")
    }


    private def tryEx(p1: String, p2: String) : Unit= {
        getRelativePath(p1, p2); Unit
    }
    @Test(expected=classOf[IllegalArgumentException]) def testEmpty1 = tryEx("", "/bbb/to.txt")
    @Test(expected=classOf[IllegalArgumentException]) def testEmpty2 = tryEx("/bbb/to.txt", "")

    @Test(expected=classOf[IllegalArgumentException]) def testNull1 = tryEx(null, "/bbb/to.txt")
    @Test(expected=classOf[IllegalArgumentException]) def testNull2 = tryEx("/bbb/to.txt", null)

    @Test(expected=classOf[IllegalArgumentException]) def testStartNotSlash1 = tryEx("bbb/to.txt", "/bbb/to.txt")
    @Test(expected=classOf[IllegalArgumentException]) def testStartNotSlash2 = tryEx("/bbb/to.txt", "bbb/to.txt")

    @Test(expected=classOf[IllegalArgumentException]) def testInvChar11 = tryEx("/b\\b", "/bbb/to.txt")
    // 以下省略(不正文字チェックのテストを対象文字1つずつ)
}

はてなシンタックスハイライトが、\エスケープやRaw Stringリテラルにうまく対応してくれない…