diff --git a/src/fsharp/FSharp.Core/string.fs b/src/fsharp/FSharp.Core/string.fs index 7e4b65f1a08..32e48bd7a6b 100644 --- a/src/fsharp/FSharp.Core/string.fs +++ b/src/fsharp/FSharp.Core/string.fs @@ -81,13 +81,38 @@ namespace Microsoft.FSharp.Core let replicate (count:int) (str:string) = if count < 0 then invalidArgInputMustBeNonNegative "count" count - if String.IsNullOrEmpty str then + let len = length str + if len = 0 || count = 0 then String.Empty + + elif len = 1 then + new String(str.[0], count) + + elif count <= 4 then + match count with + | 1 -> str + | 2 -> String.Concat(str, str) + | 3 -> String.Concat(str, str, str) + | _ -> String.Concat(str, str, str, str) + else - let res = StringBuilder(count * str.Length) - for i = 0 to count - 1 do - res.Append str |> ignore - res.ToString() + // Using the primitive, because array.fs is not yet in scope. It's safe: both len and count are positive. + let target = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked (len * count) + let source = str.ToCharArray() + + // O(log(n)) performance loop: + // Copy first string, then keep copying what we already copied + // (i.e., doubling it) until we reach or pass the halfway point + Array.Copy(source, 0, target, 0, len) + let mutable i = len + while i * 2 < target.Length do + Array.Copy(target, 0, target, i, i) + i <- i * 2 + + // finally, copy the remain half, or less-then half + Array.Copy(target, 0, target, i, target.Length - i) + new String(target) + [] let forall predicate (str:string) = diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/StringModule.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/StringModule.fs index a72ab64e236..aa486fddd6a 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/StringModule.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/StringModule.fs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. namespace FSharp.Core.UnitTests.Collections @@ -125,15 +125,42 @@ type StringModule() = [] member this.Replicate() = - let e1 = String.replicate 0 "foo" + let e1 = String.replicate 0 "Snickersnee" Assert.AreEqual("", e1) - let e2 = String.replicate 2 "foo" - Assert.AreEqual("foofoo", e2) + let e2 = String.replicate 2 "Collywobbles, " + Assert.AreEqual("Collywobbles, Collywobbles, ", e2) let e3 = String.replicate 2 null Assert.AreEqual("", e3) + let e4 = String.replicate 300_000 "" + Assert.AreEqual("", e4) + + let e5 = String.replicate 23 "天地玄黃,宇宙洪荒。" + Assert.AreEqual(230 , e5.Length) + Assert.AreEqual("天地玄黃,宇宙洪荒。天地玄黃,宇宙洪荒。", e5.Substring(0, 20)) + + // This tests the cut-off point for the O(log(n)) algorithm with a prime number + let e6 = String.replicate 84673 "!!!" + Assert.AreEqual(84673 * 3, e6.Length) + + // This tests the cut-off point for the O(log(n)) algorithm with a 2^x number + let e7 = String.replicate 1024 "!!!" + Assert.AreEqual(1024 * 3, e7.Length) + + let e8 = String.replicate 1 "What a wonderful world" + Assert.AreEqual("What a wonderful world", e8) + + let e9 = String.replicate 3 "أضعت طريقي! أضعت طريقي" // means: I'm lost + Assert.AreEqual("أضعت طريقي! أضعت طريقيأضعت طريقي! أضعت طريقيأضعت طريقي! أضعت طريقي", e9) + + let e10 = String.replicate 4 "㏖ ㏗ ℵ " + Assert.AreEqual("㏖ ㏗ ℵ ㏖ ㏗ ℵ ㏖ ㏗ ℵ ㏖ ㏗ ℵ ", e10) + + let e11 = String.replicate 5 "5" + Assert.AreEqual("55555", e11) + CheckThrowsArgumentException(fun () -> String.replicate -1 "foo" |> ignore) []