Introduction
In this blog post, I want to write on a topic, which is based on a problem not originally mine, but which I stumbled upon while reading a blog post of the same title. The orignal post is by Steve Ruiz, As someone interested in multiplayer canvases, I do love what he is doing with tldraw; a multiplayer white board web application and that was what got me to follow him on twitter and interests in his blog. By the way, you should check him out too!
This is a problem he experienced while working on tldraw, and was kind enough to blog about it, as he is also open to improving the code as written on the blog post, I took the shot and I came up with a better performing algorithm. I will describe the problem below with links to the original post containing his solution. This post is how I would solve it And in the spirit of open knowledge sharing, If you think you can improve the code even further, you can take a shot.
The Problem
In tldraw, when you create a new page, and if there's already a page with that name, then we want the second page's name to end with an incremented value, e.g. "Blog Post (1)".
This is an exact quote of the problem as described on the original post. The idea is assuming we create a new page with name "Blog Post" and then create a second page without changing the name then it gets a default "Blog Post (1)" page name. Here is an example of what he wants to achieve as described on the post.
const existing = ["Blog Post"]
function getName("Blog Post", existing) {
return "Blog Post (1)" // 0 -> 1
}
Even more test cases
const existing = ["Blog Post", "Blog Post (1)"]
function getName("Blog Post (1)", existing) {
return "Blog Post (1) (1)" // 0 -> 1
}
const existing = ["Blog Post", "Blog Post (1)!"]
function getName("Blog Post (1)!", existing) {
return "Blog Post (1)! (1)" // 0 -> 1
}
const existing = ["Blog Post", "Blog Post (1)!"]
function getName("Blog Post (1)", existing) {
return "Blog Post (1)" // no change here
}
The Solution
Here is how I would solve it.
export function getNameMap(name: string, others: Map<string, number>) {
let key = name;
if (!others.has(key)) {
others.set(key, 1);
} else {
let currentCount = Number(others.get(key));
others.set(key, currentCount + 1);
key = `${key} (${currentCount})`;
others.set(key, 1);
}
return key;
}
To explain what is happening here, The function getNameMap
accepts two argument, the name of the page to create and the existing names already created, which unlike in the original solution is a Map
. I used a Map
because it is a good data structure for what we are trying to achieve. We want to track the names that exist and the key value nature of the Map
makes it easy for us to do so.
Next I check if the page name we want to get exists on the Map
, if not, I simply add it to the Map
, using the name as the key for the map and a value of 1.
But if the page name already exists, I get the current value; which for the first duplicate would be 1, I increment it by 1 and update the existing name with this incremented value.
Then using the existing name, I append the previous value to it and set it as key in the map with initial value of 1. Eg. For an existing name Blog
appending the previous value to this name means we get Blog (1)
Summary
The original solution uses a Set
, which by design ensures items are unique, but in this solution, uses a linear operation to find and update existing name. This operation could cost at worst case O(N)
. My solution uses a Map
and lookup and updates are done in constant time O(1)
. Also, I tend to avoid using regex as I find them complicated and hard to read and I try not to use it when possible.
Here is a link to my solution and the original solution with tests.
Top comments (0)