From 59496046c6f3e4e78d38f42526be367840d0658f Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sat, 9 Aug 2025 17:43:55 -0400 Subject: [PATCH 1/3] Improve performance of `string.repeat` The `string.repeat` function now runs in loglinear time. --- CHANGELOG.md | 3 +++ src/gleam/string.gleam | 24 ++++++++++++++++++++---- test/gleam/string_test.gleam | 6 ++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c00c6aeb..ede590a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +- The performance of the `string.repeat` function has been improved. It now runs + in loglinear time. + ## v0.62.1 - 2025-08-07 - `string.inspect` now shows Erlang atoms as `atom.create("value")`, to match diff --git a/src/gleam/string.gleam b/src/gleam/string.gleam index c1e31c46..0664ac24 100644 --- a/src/gleam/string.gleam +++ b/src/gleam/string.gleam @@ -1,6 +1,7 @@ //// Strings in Gleam are UTF-8 binaries. They can be written in your code as //// text surrounded by `"double quotes"`. +import gleam/int import gleam/list import gleam/option.{type Option, None, Some} import gleam/order @@ -406,7 +407,7 @@ fn concat_loop(strings: List(String), accumulator: String) -> String { /// Creates a new `String` by repeating a `String` a given number of times. /// -/// This function runs in linear time. +/// This function runs in loglinear time. /// /// ## Examples /// @@ -416,13 +417,28 @@ fn concat_loop(strings: List(String), accumulator: String) -> String { /// ``` /// pub fn repeat(string: String, times times: Int) -> String { - repeat_loop(string, times, "") + repeat_loop(string, times, string, "") } -fn repeat_loop(string: String, times: Int, acc: String) -> String { +fn repeat_loop( + string: String, + times: Int, + doubling_acc: String, + acc: String, +) -> String { case times <= 0 { True -> acc - False -> repeat_loop(string, times - 1, acc <> string) + False -> { + let acc = case int.bitwise_and(times, 1) { + 1 -> acc <> doubling_acc + _ -> acc + } + let times = int.bitwise_shift_right(times, 1) + case times <= 0 { + True -> acc + False -> repeat_loop(string, times, doubling_acc <> doubling_acc, acc) + } + } } } diff --git a/test/gleam/string_test.gleam b/test/gleam/string_test.gleam index ed17608f..1cfc784e 100644 --- a/test/gleam/string_test.gleam +++ b/test/gleam/string_test.gleam @@ -106,8 +106,14 @@ pub fn concat_emoji_test() { } pub fn repeat_test() { + assert string.repeat("hi", times: 1) == "hi" + + assert string.repeat("hi", times: 2) == "hihi" + assert string.repeat("hi", times: 3) == "hihihi" + assert string.repeat("a", times: 10_001) |> string.length == 10_001 + assert string.repeat("hi", 0) == "" assert string.repeat("hi", -1) == "" From fee78c43e39da73b9e1f441fa5f54489d9d8cd0a Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sat, 9 Aug 2025 20:09:21 -0400 Subject: [PATCH 2/3] Simplify checking in repeat_loop --- src/gleam/string.gleam | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/gleam/string.gleam b/src/gleam/string.gleam index 0664ac24..e450c766 100644 --- a/src/gleam/string.gleam +++ b/src/gleam/string.gleam @@ -417,7 +417,10 @@ fn concat_loop(strings: List(String), accumulator: String) -> String { /// ``` /// pub fn repeat(string: String, times times: Int) -> String { - repeat_loop(string, times, string, "") + case times <= 0 { + True -> "" + False -> repeat_loop(string, times, string, "") + } } fn repeat_loop( @@ -426,19 +429,14 @@ fn repeat_loop( doubling_acc: String, acc: String, ) -> String { + let acc = case int.bitwise_and(times, 1) { + 0 -> acc + _ -> acc <> doubling_acc + } + let times = int.bitwise_shift_right(times, 1) case times <= 0 { True -> acc - False -> { - let acc = case int.bitwise_and(times, 1) { - 1 -> acc <> doubling_acc - _ -> acc - } - let times = int.bitwise_shift_right(times, 1) - case times <= 0 { - True -> acc - False -> repeat_loop(string, times, doubling_acc <> doubling_acc, acc) - } - } + False -> repeat_loop(string, times, doubling_acc <> doubling_acc, acc) } } From 63b8bdab717b939641c3a531896c13a356b936ee Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sun, 10 Aug 2025 12:48:51 -0400 Subject: [PATCH 3/3] Use % and / over bitwise operations --- src/gleam/string.gleam | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/gleam/string.gleam b/src/gleam/string.gleam index e450c766..e00d0327 100644 --- a/src/gleam/string.gleam +++ b/src/gleam/string.gleam @@ -1,7 +1,6 @@ //// Strings in Gleam are UTF-8 binaries. They can be written in your code as //// text surrounded by `"double quotes"`. -import gleam/int import gleam/list import gleam/option.{type Option, None, Some} import gleam/order @@ -429,11 +428,11 @@ fn repeat_loop( doubling_acc: String, acc: String, ) -> String { - let acc = case int.bitwise_and(times, 1) { + let acc = case times % 2 { 0 -> acc _ -> acc <> doubling_acc } - let times = int.bitwise_shift_right(times, 1) + let times = times / 2 case times <= 0 { True -> acc False -> repeat_loop(string, times, doubling_acc <> doubling_acc, acc)