【问题标题】:F# Join Strings with Oxford CommaF# 用牛津逗号连接字符串
【发布时间】:2019-04-10 03:43:18
【问题描述】:

我想使用牛津(或串行)逗号将一组字符串连接成一个字符串。

给定

let ss = [ "a"; "b"; "c"; "d" ]

我想要

"a, b, c, and d"

这是我想出的。

let oxford (strings: seq<string>) =
  let ss = Seq.toArray strings
  match ss.Length with
  | 0 -> ""
  | 1 -> ss.[0]
  | 2 -> sprintf "%s and %s" ss.[0] ss.[1]
  | _ ->
    let allButLast = ss.[0 .. ss.Length - 2]
    let commaSeparated = System.String.Join(", ", allButLast)
    sprintf "%s, and %s" commaSeparated (Seq.last ss)

如何改进?

--- 编辑 ---
关于多次迭代序列的评论是正确的。以下两种实现都避免了转换为数组。

如果我使用seq,我很喜欢这个:

open System.Linq
let oxfordSeq (ss: seq<string>) =
  match ss.Count() with
  | 0 -> ""
  | 1 -> ss.First()
  | 2 -> sprintf "%s and %s" (ss.ElementAt(0)) (ss.ElementAt(1))
  | _ ->
    let allButLast = ss.Take(ss.Count() - 1)
    let commaSeparated = System.String.Join(", ", allButLast)
    sprintf "%s, and %s" commaSeparated (ss.Last())

如果我使用array,我还可以利用索引来避免Last()的迭代。

let oxfordArray (ss: string[]) =
  match ss.Length with
  | 0 -> ""
  | 1 -> ss.[0]
  | 2 -> sprintf "%s and %s" ss.[0] ss.[1]
  | _ ->
    let allButLast = ss.[0 .. ss.Length - 2]
    let commaSeparated = System.String.Join(", ", allButLast)
    sprintf "%s, and %s" commaSeparated (ss.[ss.Length - 1]

--- 编辑 ---
从@CaringDev 看到该链接,我认为这非常好。没有通配符,处理 null,更少的索引才能正确,并且只在 Join() 方法中遍历数组一次。

let oxford = function
    | null | [||] -> ""
    | [| a |] -> a
    | [| a; b |] -> sprintf "%s and %s" a b
    | ss ->
        let allButLast = System.ArraySegment(ss, 0, ss.Length - 1)
        let sb = System.Text.StringBuilder()
        System.String.Join(", ", allButLast) |> sb.Append |> ignore
        ", and " + ss.[ss.Length - 1] |> sb.Append |> ignore
        string sb

这一次也很不错,跳的更少了:

let oxford2 = function
    | null | [||] -> ""
    | [| a |] -> a
    | [| a; b |] -> sprintf "%s and %s" a b
    | ss ->
        let sb = System.Text.StringBuilder()
        let action i (s: string) : unit = 
            if i < ss.Length - 1 
            then 
                sb.Append s |> ignore
                sb.Append ", " |> ignore
            else 
                sb.Append "and " |> ignore
                sb.Append s |> ignore
        Array.iteri action ss          
        string sb

【问题讨论】:

  • 有什么需要改进的地方?
  • 我想知道是否有一些不必要的字符串分配,或者它是否可以更具可读性。
  • 我也想知道是否有更好的方法来获取allButLast。
  • 这更适合Code Review

标签: .net string collections f#


【解决方案1】:

您可以直接查看列表并在列表中使用模式匹配。也许这可以改进,但它给出了这个想法。

let rec oxford (s:string) (ss:string list) =
    match ss with
    | [] -> s
    | [x;y] -> sprintf "%s, %s, and %s" s x y
    | h::t when String.length s = 0 -> oxford h t
    | h::t -> oxford (sprintf "%s, %s" s h) t

它递归地调用自己,并使用一个较小的列表应用逗号。当列表只有 2 大小时,它使用 and 代替。 when 很不幸,但发现我第一次使用空字符串调用时需要它,所以不要以 , 结尾。

编辑

所以我个人更喜欢上面的选项用于少量单词。但是,每次调用的字符串 concat 对大数字的表现不佳。

// collect into a list including the *,* and *and*, then just concat that to string
let oxfordDrct (ss:string list) =
    let l = ss |> List.length
    let map i s = if(i < l-1) then [s;", "] else ["and ";s]        
    match ss with
    | [] -> ""
    | [x] -> x
    | [x;y] -> sprintf "%s, and %s" x y
    | _ -> ss |> List.mapi map |> List.concat |> String.concat ""

// Recursive like the original but instead pass a StringBuilder instead of string
let oxfordSb xs =        
    let rec collect (s:StringBuilder) (ss:string list) =
        match ss with
        | [] -> s
        | [x;y] -> sprintf ", %s, and %s" x y |> s.Append
        | h::t when s.Length = 0 -> collect (s.Append(h)) t
        | h::t -> collect (s.Append(sprintf ", %s" h)) t
    let sb = new StringBuilder()     
    (collect sb xs) |> string

这 2 个选项的性能与原始选项非常相似,都比 rec 优于 string。

【讨论】:

  • 非常有趣的答案。我通常不考虑递归。我喜欢我通过接受 seq (IEnumerable) 使我的答案通用,因此它适用于列表、数组、seq 等。我从列表模式匹配开始,但决定我不想被限制接受列表.我本可以接受一个 seq 并将其转换为一个列表。
  • 你为什么决定接受一个字符串和一个字符串列表而不是一个字符串列表?
  • @BrettRowberry 对于递归函数,您需要一种方法来收集累积的结果。 WRT rec 在 FP 中比较常见。同样重要的是理解的容易程度。
【解决方案2】:

foldBack 也是一种可能。以这种方式连接字符串的性能并不出色,但对于 4 个项目,它通常无关紧要。

let oxfordify (ws : seq<string>) : string =
  // Folder concats the value and the aggregated result using seperator 0
  //  it updates the state with the new string and moves 
  //  seperator 1 into seperator 0 slot and set seperator 1 to ", "
  let folder v (r, s0, s1) = (v + s0 + r, s1, ", ")
  // The seperator 0 for first iteration is empty string (if it's only 1 value)
  // The seperator 1 is set to ", and " as the seperator between 2 last items
  // For all other items ", " will be used (see folder)
  let r, _, _ = Seq.foldBack folder ws ("", "", ", and ")
  r

一位同事向我指出了使用无限序列表示分隔符的方向:

let separators = 
  Seq.concat [| [|""; ", and "|] :> seq<_>; Seq.initInfinite (fun _ -> ", ") |]
let oxfordify (ws : seq<string>) : string = 
  Seq.fold2 (fun r v s -> v + s + r) "" (ws |> Seq.rev) separators 

要获得更高性能的选项,您可以考虑这样的事情:

module Details =
  module Loops =
    let inline app (sb : System.Text.StringBuilder) (w : string) : unit =
      sb.Append w |> ignore
    let rec oxfordify sb (ws : _ array) i : string =
      if i < ws.Length then
        if i = 0 then 
          ()
        elif i = ws.Length - 1 then 
          app sb ", and "
        else 
          app sb ", "
        app sb ws.[i]
        oxfordify sb ws (i + 1)
      else
        sb.ToString ()
open Details

let oxfordify (ws : string array) : string = 
  let sb = System.Text.StringBuilder ()
  Loops.oxfordify sb ws 0

【讨论】:

  • 不错。简洁和使用可用库函数的要点。我想到了这一点,但递归对我来说更容易理解。我想在幕后与递归没有很好的性能大致相同。
  • 谢谢大家!有人说 StackOverflow 不友好,但 F#ers 是一群特殊的人 :-)
【解决方案3】:

我不那么不同的方法。

let oxford (ss: string array) =
    match ss.Length with
    | 0 -> ""
    | 1 -> ss.[0]
    | 2 -> sprintf "%s and %s" ss.[0] ss.[1]
    | _ ->
       let cs = System.String.Join(", ", ss.[ 0 .. ss.Length - 2])
       sprintf "%s, and %s" cs (ss.[ss.Length - 1])

我真的看不出你的代码有什么问题,除了你会多次迭代你的序列(数组转换 + 字符串连接 + Seq.last)。

这不是性能问题,因为我不希望在大于一位数的序列上调用此函数,但如果序列有副作用或计算成本很高,您将得到奇怪的行为。这就是我将输入切换到数组的原因。

就可读性而言,你不能比你已经拥有的更好,明确枚举基本情况,并且从最后一行的sprintf 分配额外的字符串无论如何都是无关紧要的(尤其是与你的相比) d 从直接递归中得到)。

【讨论】:

  • 非常好。 ss.[0 .. ss.Length - 2] 是否分配?
  • 是的,数组拼接运算符分配了一个新数组。为避免这种情况,您可以使用截断的序列:Seq.take (ss.Length - 1) ss 或数组段:System.ArraySegment(ss, 0, ss.Length - 1)
【解决方案4】:

一种我还没见过的方式:在列表末尾匹配。

let ox =
    List.rev >> function
    | [] -> ""
    | [x] -> x
    | [y; x] -> x + " and " + y
    | y::ys -> String.concat ", " (List.rev ("and " + y::ys))
// val ox : (string list -> string)

ox["a"; "b"; "c"; "d"]
// val it : string = "a, b, c, and d"

【讨论】:

    【解决方案5】:

    另一个递归变体:

    let rec oxford l =
        match l with
        | [] -> ""
        | [x] -> x
        | [x; y] -> x + " and " + y
        | head :: tail -> 
            head + ", " + oxford tail
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-05-27
      • 1970-01-01
      • 1970-01-01
      • 2017-09-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-10-24
      相关资源
      最近更新 更多