views:

126

answers:

5

I want to extract the middle part of a string using FSharp if it is quoted, similar like this:

let middle =
    match original with
    | "\"" + mid + "\"" -> mid
    | all -> all

But it doesn't work because of the infix operator + in pattern expression. How can I extract this?

+7  A: 

I don't think there is any direct support for this, but you can certainly write an active pattern. Active patterns allow you to implement your own code that will run as part of the pattern matching and you can extract & return some part of the value.

The following is a pattern that takes two parameters (prefix and postfix string) and succeeds if the given input starts/ends with the specified strings. The pattern is not complete (can fail), so we'll use the |Name|_| syntax and it will need to return option value:

let (|Middle|_|) prefix postfix (input:string) =
  // Check if the string starts with 'prefix', ends with 'postfix' and 
  // is longer than the two (meaning that it contains some middle part)
  if input.StartsWith(prefix) && input.EndsWith(postfix) && 
     input.Length >= (prefix.Length + postfix.Length) then 
    // Strip the prefix/postfix and return 'Some' to indicate success
    let len = input.Length - prefix.Length - postfix.Length
    Some(input.Substring(prefix.Length, len))
  else None // Return 'None' - string doesn't match the pattern

Now we can use Middle in pattern matching (e.g. when using match):

match "[aaa]"  with
| Middle "[" "]" mid -> mid
| all -> all
Tomas Petricek
Your implementation strategy is better than mine (only one call to `Substring`), but glad to see we had the same idea! :)
Brian
Thanks Tomas. Your answer is so clear and helpful. Thanks to brian and kvb also. Unfortunately, I am new in StackOverflow and do not have enought credits to make your answers up.
newwave
@Brian: Yes, it looks like we posted almost the same answer! I also considered your strategy (but thought that checking length is easier) and I also considered passing the two arguments using a tuple :-)
Tomas Petricek
+1  A: 

Patterns have a limited grammar - you can't just use any expression. In this case, I'd just use an if/then/else:

let middle (s:string) =
  if s.[0] = '"' && s.[s.Length - 1] = '"' && s.Length >= 2 then 
    s.Substring(1,s.Length - 2)
  else s

If grabbing the middle of a string with statically known beginnings and endings is something that you'll do a lot, then you can always use an active pattern as Tomas suggests.

kvb
Thanks kvb. I prefer Tomas' answer just because I do not want to use imperative way.
newwave
+2  A: 

Parameterized active patterns to the rescue!

let (|HasPrefixSuffix|_|) (pre:string, suf:string) (s:string) =
    if s.StartsWith(pre) then
        let rest = s.Substring(pre.Length)
        if rest.EndsWith(suf) then
            Some(rest.Substring(0, rest.Length - suf.Length))
        else
            None
    else
        None

let Test s =
    match s with
    | HasPrefixSuffix("\"","\"") inside -> 
        printfn "quoted, inside is: %s" inside
    | _ -> printfn "not quoted: %s" s

Test "\"Wow!\""
Test "boring"
Brian
Parameterized active patterns are amazingly powerful.
gradbot
Seriously nifty! :-)
Jon Harrop
+2  A: 

… or just use plain old regular expression

let Middle input =
    let capture = Regex.Match(input, "\"([^\"]+)\"")
    match capture.Groups.Count with
    | 2 -> capture.Groups.[1].Value
    | _ -> input
Artem K.
Might be better to return an option type?
Jon Harrop
Author wants to return the whole string if it's not quoted, as I understood the question.
Artem K.
A: 

Not sure how efficient this is:

let GetQuote (s:String) (q:char) = 
        s 
        |> Seq.skip ((s |> Seq.findIndex (fun c -> c = q))+1)
        |> Seq.takeWhile (fun c-> c <> q) 
        |> Seq.fold(fun acc c -> String.Format("{0}{1}", acc, c)) ""

Or there's this with Substring in place of the fold:

let GetQuote2 (s:String) (q:char)  = 
    let isQuote = (fun c -> c = q)
    let a = (s |> Seq.findIndex isQuote)+1
    let b = ((s |> Seq.take(a) |> Seq.findIndex isQuote)-1)
    s.Substring(a,b);

These will get the first instance of the quoted text anywhere in the string e.g "Hello [World]" -> "World"

jdoig