F # Соединить строки с запятой Оксфорд - PullRequest
3 голосов
/ 06 апреля 2019

Я хочу объединить коллекцию строк в одну строку, используя оксфордскую (или последовательную) запятую.

Учитывая

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, я думаю, это довольно мило. Никаких подстановочных знаков, обрабатывает нуль, меньше индексации, чтобы получить право, и только обход массива один раз в методе 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

Ответы [ 5 ]

5 голосов
/ 06 апреля 2019

Вы можете посмотреть прямо в списке и использовать сопоставление с образцом в списке.Может быть, это можно улучшить, но это дает идею.

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, вместо него используются и .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.

3 голосов
/ 06 апреля 2019

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
2 голосов
/ 06 апреля 2019

Мой не такой уж и другой подход.

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 в последней строке в любом случае неуместно (особенно по сравнению с тем, что вы получаю от прямой рекурсии).

1 голос
/ 09 апреля 2019

Способ, которого я еще не видел: Совпадение в конце списка.

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"
0 голосов
/ 09 апреля 2019

Еще один рекурсивный вариант:

let rec oxford l =
    match l with
    | [] -> ""
    | [x] -> x
    | [x; y] -> x + " and " + y
    | head :: tail -> 
        head + ", " + oxford tail
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...