Last active
July 18, 2025 02:14
-
-
Save tigregalis/e8e50764f9d8580f357833af76a91b78 to your computer and use it in GitHub Desktop.
DelimitedStringBuilder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::fmt::Write; | |
use std::fmt::Arguments; | |
/// This type simulates pushing into a `Vec<String>` and similar, | |
/// and then joining with a delimiter, | |
/// but it's optimised for minimising allocations. | |
/// It does zero allocations of its own. | |
/// | |
/// TODO: Instead of String use a Writer, | |
/// then you could write directly to a stream. | |
pub struct DelimitedStringBuilder<'string, P: AsRef<str>> { | |
pattern: P, | |
string: &'string mut String, | |
first: bool, | |
} | |
impl<'string, P: AsRef<str>> DelimitedStringBuilder<'string, P> { | |
pub fn new(string: &'string mut String, pattern: P) -> Self { | |
string.clear(); | |
Self { | |
pattern, | |
string, | |
first: true, | |
} | |
} | |
/// Push a string. | |
/// | |
/// NOTE: Instead of `sb.push_str(format!("{foo}"))` | |
/// use `sb.push_fmt(format_args!("{foo}"))`. | |
/// See [`push_fmt`]; | |
pub fn push_str(&mut self, s: impl AsRef<str>) { | |
if !self.first { | |
self.string.push_str(self.pattern.as_ref()); | |
} else { | |
// Hint to the compiler that this branch is unlikely | |
cold(); | |
self.first = false; | |
} | |
self.string.push_str(s.as_ref()); | |
} | |
/// Push a formatted string ([`Arguments`]), | |
/// which avoids allocation of the [`format`] macro. | |
/// | |
/// We unfortunately can't just implement `Write` and use `write!` for this type | |
/// because it is semantically incorrect: | |
/// `write!` will call `write_fmt` for each argument, | |
/// resulting in unintentional delimiters being written as well, | |
/// since there's no way to observe that we are in the middle of writing a single element. | |
/// | |
/// ``` | |
/// let mut s = String::new(); | |
/// let mut sb = DelimitedStringBuilder::new(&mut s, " "); | |
/// let name = "Alice"; | |
/// sb.push_fmt(format_args!("{name}")); | |
/// ``` | |
pub fn push_fmt(&mut self, args: Arguments<'_>) { | |
if !self.first { | |
self.string.push_str(self.pattern.as_ref()); | |
} else { | |
// Hint to the compiler that this branch is unlikely | |
cold(); | |
self.first = false; | |
} | |
self.string.write_fmt(args).ok(); | |
} | |
/// "Clear" the builder. Equivalent to calling `Vec::clear()`. | |
pub fn clear(&mut self) { | |
self.string.clear(); | |
self.first = true; | |
} | |
pub fn get(&self) -> &str { | |
&self.string | |
} | |
pub fn is_empty(&self) -> bool { | |
self.first | |
} | |
// TODO: implement other Vec- / collection- / Iterator-like methods | |
} | |
/// Hint to the compiler that this branch is unlikely | |
#[inline(always)] | |
#[cold] | |
fn cold() {} | |
fn main() { | |
let mut buf = String::new(); | |
let mut sb = DelimitedStringBuilder::new(&mut buf, " ".to_owned()); | |
sb.push_str("Hello,"); | |
sb.push_str("world!"); | |
println!("{}", sb.get()); | |
sb.clear(); | |
sb.push_str("It's"); | |
sb.push_str("time"); | |
sb.push_str("for"); | |
let exclamations = "!".repeat(rand::random_range(1..=10)); | |
sb.push_fmt(format_args!("revolution{exclamations}")); | |
println!("{}", sb.get()); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment