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

ふもさんの「お題:フルパスから相対パスを求める」をF# でやってみた

お題:フルパスから相対パスを求める - No Programming, No Life
なんか盛り上がっているらしいので私も便乗してみた。

追記

2011/9/1追記:コメントでご指摘を受けた部分を修正+実装変更
2011/9/1追記:タプル→カリー化に変更+テストコードもあわせて修正。MaybeBuilder.Bindのリファクタリング

コード

(* RelativePath.fs *)
module RelativePath

open System.Text.RegularExpressions

type MaybeBuilder() =

    member this.Bind(x,f) =
        Option.bind f x

    member this.Return(x) =
        Some x

let maybe = new MaybeBuilder()

let relativizePath fromA toB =

    let trySplit (path:string) =
        match Regex.Match(path,"^[^/]|\.$|^$|[\\?*:|\"<>]|//").Success with
            | true -> None
            | false -> Some (path.Split([|'/'|]) |> Array.toList)
    
    let addPath =
        function 
        | "" -> "./"
        | path -> path

    let rec calc f t path =
        match f, t with
            | [x], _ -> t |> List.reduce (fun a b -> a + "/" + b) |> (+) (addPath path)
            | x::xs, y::ys when x = y -> calc xs ys path
            | _, _ -> calc (f |> List.tail) t (path+"../")

    maybe {
        let! f = fromA |> trySplit
        let! t = toB |> trySplit
        return calc f t ""
    }
(* RelativePathScenario.fs *)
module RelativePathScenario

open NaturalSpec
open RelativePath

[<Example("/aaa/bbb/from.txt","/aaa/bbb/to.txt","./to.txt")>]
[<Example("/aaa/bbb/from.txt","/aaa/to.txt","../to.txt")>]
[<Example("/aaa/bbb/from.txt","/aaa/bbb/ccc/to.txt","./ccc/to.txt")>]
[<Example("/aaa/bbb/from.txt","/aaa/ccc/ddd/to.txt","../ccc/ddd/to.txt")>]
[<Example("/aaa/bbb/from.txt","/ddd/ccc/to.txt","../../ddd/ccc/to.txt")>]
[<Example("/aaa/bbb/","/aaa/ddd/to","../ddd/to")>]
[<Example("/aaa/bbb/from","/aaa/ccc/","../ccc/")>]
[<Example("/aaa/bbb/","/aaa/ccc/","../ccc/")>]
[<Example("/aaa/bbb/ccc.txt","/aaa/bbb/ccc.txt","./ccc.txt")>]
let ``二つのフルパスを受け取り一つ目のパスから二つ目のパスへの相対パスを返す`` f t result =
    Given (f, t)
    ||> When relativizePath
    |> It should equal (Some result)
    |> Verify

[<Example("","/bbb/to.txt")>]
[<Example("/aaa/g*","/bbb/to.txt")>]
[<Example("aaa/bbb/from.txt","./bbb/to.txt")>]
[<Example("//aaa////bbb/from.txt","/////aaa//////bbb///////to.txt")>]
let ``どちらかのパスが無効値ならNone`` f t =
    Given (f, t)
    ||> When relativizePath
    |> It should equal None
    |> Verify

ついでにNaturalSpecを使ってTDDで実装してみたものの、例5〜例8まではテスト追加するだけよかっただけど。
正規表現を含め色々とあってる自信がないわけだが…テスト動いてるしいいよね?
あと別の人が書けばもっときれいにかけるのではないかなとも思っている。
Gistにはそのうちあげます*1

*1:文字化け対策してなかった