Generating abbreviations for strings One of many aspects of a project I am responsible for is generating an abbreviation, that is no more than n characters, for a given string. We use this in the organization for a couple things, and it only happens once for each "thing", so performance isn't hyper-critical.

An example is something like "The Main Test Company" &mdash; when asking for a 3-character abbreviation, I want to be able to generate a string like MTC. Now this would also be the same string as My Tiny Cat, but the idea is that a given company can be abbreviated and it can be done safely and effectively.

Also, if the abbreviations aren't large enough for the requested characters (say we request 4 characters from My Awesome Company) it should, for each abbreviation character, start adding characters to the total (so our example is MYAC).

I wrote the whole thing in F#, and it seems pretty readable and followable, I'm also including some test strings to verify functionality. Any comments are welcome.

let genAbbr ignoreList padding charCount (str : string) =
    if str.Length &lt;= charCount then
        match padding with
        | Some c -&gt; str |&gt; Seq.append (String(c, charCount - str.Length)) |&gt; Seq.toArray
        | None -&gt; str.ToCharArray()
        |&gt; Array.map Char.ToUpper
        |&gt; String
    else
        let words =
            [|' '|]
            |&gt; Array.append (str.ToCharArray())
            |&gt; Array.fold (fun (acc, cs) c -&gt;
                match c |&gt; Char.IsUpper, c = ' ', cs with
                | true, _, [] -&gt; (acc, [c])
                | true, _, cs -&gt; (cs::acc, [c])
                | _, true, [] -&gt; (acc, [])
                | _, true, cs -&gt; (cs::acc, [])
                | _, _, cs -&gt; (acc, c::cs)) ([], [])
            |&gt; fst
            |&gt; Array.ofList
            |&gt; Array.map (Array.ofList &gt;&gt; Array.rev &gt;&gt; String)
            |&gt; Array.rev
            |&gt; Array.map (fun s -&gt; s.ToCharArray())
        let ignoreWords = words |&gt; Array.filter (fun w -&gt; ignoreList |&gt; Array.contains ((w |&gt; String).ToLower()) |&gt; not)
        let abbrCs =
            if ignoreWords |&gt; Array.length &gt;= charCount then ignoreWords |&gt; Array.map Seq.head
            else if words |&gt; Array.length &gt;= charCount then words |&gt; Array.map Seq.head
            else
                let firstCaps = words |&gt; Array.map (fun s -&gt; if s.Length &gt; charCount then s.[0..charCount] else s) |&gt; Array.length
                printfn "%A %A %A" ignoreWords words firstCaps
                words
                |&gt; Array.fold (fun (acc, r) w -&gt;
                    if w.Length &gt; r then (w.[0..(r - 1)]::acc, 1)
                    else (w::acc, r - (w.Length - 1))) ([], charCount - firstCaps + 1)
                |&gt; fst
                |&gt; Array.ofList
                |&gt; Array.rev
                |&gt; Array.concat
                |&gt; Array.map Char.ToUpper
        abbrCs |&gt; Array.take charCount |&gt; String


And lastly, some POC's:

["The Main Test Company"; "Main Test Company"; "The Main Company"; "The Company"; "Company"; "MainCompany"; "The MainCompany"; "MainTestCompany"; "SomeRX"; "SomeCompanyT"]
|&gt; List.map (genAbbr [|"the"|] None 3)
// Should be ["MTC"; "MTC"; "TMC"; "THC"; "COM"; "MAC"; "TMC"; "MTC"; "SRX"; "SCT"]


Thanks for any commentary, and I look forward to this being brutally decimated. :)
Some possible improvement points:


Splitting out functions for word grouping, padding, and word filtering
Removing unnecessary branching
Moving from fold to self-contained recursive functions that require less post processing
Use a HashSet with a StringComparer for filtering


Using these techniques, and avoiding regexes (like my previous answer), we can get a solution that may be easier to reason about:

    open System.Collections.Generic

    let getWords (str:string) =
        let word rev_chars = 
            (rev_chars |&gt; List.rev |&gt; Array.ofList |&gt; String).ToUpper()

        let add_word rev_chars acc = 
            if rev_chars &lt;&gt; [] then (word rev_chars) :: acc else acc

        let rec loop (idx:int) (rev_chars:char list) (acc: string list) prev_lower =
            if idx &lt; str.Length then
                let c = str.[idx]
                let rev_chars, acc =
                    match str.[idx] with
                    | ' '                                      -&gt; []             , (add_word rev_chars acc)
                    | c when Char.IsUpper(c)                   -&gt; [c]            , (add_word rev_chars acc)
                    | c                                        -&gt; (c::rev_chars) , acc
                loop (idx + 1) rev_chars acc (Char.IsLower c)
            else
                let acc = add_word rev_chars acc in List.rev acc

        loop 0 [] [] false

    let pad (str:string) len padchar =
        if str.Length &lt; len then str + String(padchar, (len - str.Length)) else str

    // Filter a list of words, if we can 'afford' it, starting from the back    
    let filterWords (ignoreList:HashSet&lt;string&gt;) charCount words =
        let rec loop revWords acc =
            match revWords with
            | [] -&gt; acc
            | word::revWords when ignoreList.Contains(word) -&gt;
                let getLen = List.fold (fun len s -&gt; len + String.length s) 0            
                let maxLenWithout = (getLen words) + (getLen acc)
                let numOtherParts = List.length revWords + List.length acc
                if maxLenWithout &gt;= charCount &amp;&amp; numOtherParts &gt;= charCount then loop revWords acc
                else loop revWords (word::acc)
            | word::revWords -&gt; loop revWords (word::acc)

        loop (List.rev words) []

    let genAbbr (ignoreList:HashSet&lt;string&gt;) padding charCount (str : string) =
        let words = getWords str |&gt; filterWords ignoreList charCount

        // Given a list of words, generate a sequence of abbreviations
        let rec abbrs before (words: string list) : string seq = seq {
                match words with
                | [] -&gt; ()
                | word::words -&gt;
                    for i = 1 to word.Length do
                        let before = before + word.Substring(0, i)
                        yield before + (words |&gt; List.map (fun w -&gt; w.Substring(0, 1)) |&gt; String.concat "")
                    yield! abbrs (before + word) words }

        (abbrs "" words)
        |&gt; Seq.takeWhile (fun s -&gt; s.Length &lt;= charCount)
        |&gt; Seq.maxBy (fun s -&gt; s.Length)
        |&gt; fun s -&gt;
            match padding with | None -&gt; s | Some padchar -&gt; pad s charCount padchar


To call it:

    let ignored = let h = HashSet&lt;string&gt;(StringComparer.OrdinalIgnoreCase) in h.Add("the") |&gt; ignore; h

    ["The Main Test Company"; "Main Test Company"; "The Main Company"; "The Company"; "Company"; "MainCompany"; "The MainCompany"; "MainTestCompany"; "SomeRX"; "SomeCompanyT"]
    |&gt; List.map (fun w -&gt; w,  (genAbbr ignored None 3 w))