String.prototype.with()

Hello.

I like Array.prototype.with(), but I really would love to see the same thing done with strings, hence the title - String.prototype.with().

Recently, I needed to replace a character in a string, knowing only its index. The process is kind of tedious:

string.substring(0, index) + newChar + string.substring(index + 1)

This way, you cannot chain string operations, which makes things even longer.

If we have String.prototype.with(), then all that becomes

string.with(index, newChar)

and it can be chained for further manipulations.

Bonus: The method name and function match the ones in Array, so it is easier on the programmer.

3 Likes

Can you elaborate on your concrete use cases?

2023-07-12s13:55
2023-07-12j14:25
2023-07-12f14:53
2023-07-12015:05

Well, I can give you my use case, though I'm sure it will scare you more than convince you. I'm working on a very old system, and I need to display a list of events and an icon for the event that happened. Someone thought it would be smart to actually "encode" them all into one string. As you can see, each string actually holds a date, but there is this seemingly random character between the date and time - that is an icon from a custom web font. So I really do not know what character I'll face there, but I know it is always in that spot. I use .charAt() to get the character for the font, and then I need to replace it with ' ' (space) and display it to the user. Sure, those characters are finite, I can do multiple .replace('s', ' ').replace('j'. ' ')... but that is not sustainable - what happens if tomorrow there is a new event type?

As I said, this is scary stuff, but I'm sure other people also get themselves into supporting old stuff.

Also, I see other people asking on Stack Overflow for this functionality. I do not know their use case (they do not explain), but it might be better than mine. string - How do I replace a character at a particular index in JavaScript? - Stack Overflow

PS: I do not need chaining in this case, I've added it to the proposal because it is an added benefit.

str.replace(/^\d{4}-\d{2}-\d{2}(.)/, (char) => iconFor(char)) or similar? The regex could probably even be simpler, /^.{10}(.)/.

Regex can solve a lot of things, but it makes code harder to read and sometimes (as in this case) bigger. String.prototype.with() is more elegant, easy to understand, and has a smaller footprint. Also, I'm not sure if some other case can be solved with regex, though it's very much possible.

I don't think you're going to find much support for adding new code-unit-based operations on String.prototype. Your slicing solution seems good enough to me.

Your slicing solution seems good enough to me.

It doesn't look great, it's very long and has a high chance of introducing off-by-one errors. String#with sounds like a very reasonable counterpart to Array#with. Array even already had .splice do to the same thing (x=[...arr];x.splice()), and yet .with was added.

2 Likes

By querying Sourcegraph I already found some usages of it:

A notable example from one of the above is to replace the character at the current cursor position with a blinker in a terminal or editor. Perhaps we can go with the more generic toSpliced but the with method just perfectly fits this use case.

1 Like

Question: how many of these in practice are combined with .indexOf or similar (like a text processing loop or similar parser)? Most of those could easily be replaced with regular expressions, ones that are more than just string.replace(/^.{n}(.)/s, "f") (which is what your string.with(n, "f") mostly is)?

In virtually all the cases the position isn’t a constant – at least in all of the above examples given. I don’t think regex composing is a good solution.

And almost all of those that can't be replaced with regular expressions but still need to do some string slicing need to track an explicit start offset in some way or another as well.

If you're doing a lot of cursor replacements, strings will become a perf and/or memory cliff for you over time. You'll need to switch to a specialized data structure like a gap buffer for that.

I've just never come across a use case for .with personally that was compelling enough to merit a whole language extension.


To address three of your links, the authors of each just severely overcomplicated their problems.

// node-menu.js lines 26-44
var normalizedNodeId = hash.nodeId
    .replace(":", ":​")
    .replaceAll(".", ".​")

// tutorial-utils.js lines 1-16
function getLabelName(innerHTML) {
    return innerHTML.replace(/[^\d_a-z]/g, m => {
        if (m === "+") return "p"
        if (m === " ") return "_"
        if (m === "#") return "sharp"
        return `ascii${m.charCodeAt(0)}`
    })
}

// parse-url.js lines 17-39
const correctProtocol = (arg, protocols) => {
    const parts = /^(.*?:)?(.*?@)?(.*)$/.exec(arg)
    if (Object.hasOwn(protocols, parts[1])) return arg
    if (parts[1].includes("@")) return arg
    if (parts[2]) return `git+ssh://${parts[2]}${parts[3]}`
    if (parts[3].startsWith("//")) return arg
    return `${parts[1]}//${parts[3]}`
}

The other two, placeholder.js line 52 and util.js line 41, don't have a real alternative beyond two slices. But they're also not really simplified by it - the first is among a long list of cases, and the second's in an unavoidably very complicated loop. Still not convinced.