Skip to content

Instantly share code, notes, and snippets.

@ZeikJT
Last active December 6, 2023 04:43
Show Gist options
  • Save ZeikJT/6089265e5eda6c00c4c0038f7d14b5b7 to your computer and use it in GitHub Desktop.
Save ZeikJT/6089265e5eda6c00c4c0038f7d14b5b7 to your computer and use it in GitHub Desktop.
Concatenating and substringing strings without doing native string operations in JS
class MagicString {
static #convertStringToData(str) {
return {str, start: 0, length: str.length}
}
static #makeDataCopy(data) {
return data.map((data) => ({...data}))
}
static #splitData(index, data) {
let offset = index
const offsetIndex = data.findIndex(({length}) => {
if (offset < length) {
return true
}
offset -= length
return false
})
// If the offset doesn't fall between two string pieces we need to split the piece of data it lands between.
if (offset !== 0) {
const dataAtIndex = data[offsetIndex]
const dataAfterIndex = {str: dataAtIndex.str, start: dataAtIndex.start + offset, length: dataAtIndex.length - offset}
dataAtIndex.length = offset
// If the data we're splitting up is already at the end we can just push instead of splice.
if (offsetIndex === data.length - 1) {
data.push(dataAfterIndex)
} else {
// Could also split this into unshift vs splice but the gain there seems negligible.
data.splice(offsetIndex + 1, 0, dataAfterIndex)
}
}
return offsetIndex + 1
}
static makeSubstring(str, start, end) {
return new MagicString(String(str)).substring(start, end)
}
constructor(...strs) {
this.data = []
this.length = 0
this.append(...strs)
}
toString() {
return this.data.reduce((concat, {str, start, length}) => concat + str.substring(start, start + length), '')
}
prepend(...strs) {
for (const str of strs) {
if (!str || !str.length) {
continue
}
if (str instanceof MagicString) {
this.data = [...MagicString.#makeDataCopy(str.data), ...this.data]
this.length += str.length
} else {
const definitelyStr = String(str)
this.data.unshift(MagicString.#convertStringToData(definitelyStr))
this.length += definitelyStr.length
}
}
return this
}
append(...strs) {
for (const str of strs) {
if (!str || !str.length) {
continue
}
if (str instanceof MagicString) {
this.data = [...this.data, ...MagicString.#makeDataCopy(str.data)]
this.length += str.length
} else {
const definitelyStr = String(str)
this.data.push(MagicString.#convertStringToData(definitelyStr))
this.length += definitelyStr.length
}
}
return this
}
substring(start = 0, end = this.length) {
// Check for valid indexes.
if ((start > end) || (start < 0) || (end > this.length)) {
throw new Error(`Invalid start (${start}) or end (${end}) values`)
}
// If the two are the same, we're substringing down to nothing, make it easy.
if (start === end) {
this.data = []
this.length = 0
return this
}
let removeFromStart = start
while (removeFromStart > 0) {
const firstStr = this.data[0]
// If the amount to trim off fits into the first piece, trim and stop there.
if (removeFromStart < firstStr.length) {
firstStr.start += removeFromStart
firstStr.length -= removeFromStart
break
}
// The amount to remove from the start is equal to or longer than the first piece, remove it entirely.
this.data.shift()
removeFromStart -= firstStr.length
}
let removeFromEnd = this.length - end
while (removeFromEnd > 0) {
const lastStr = this.data[this.data.length - 1]
// If the amount to trim off fits into the first piece, trim and stop there.
if (removeFromEnd < lastStr.length) {
lastStr.length -= removeFromEnd
break
}
// The amount to remove from the end is equal to or longer than the last piece, remove it entirely.
this.data.pop()
removeFromEnd -= lastStr.length
}
this.length = end - start
return this
}
insert(start, str) {
// Check for valid start.
if ((start < 0) || (start > this.length)) {
throw new Error(`Invalid start (${start}) value`)
}
if (!str || !str.length) {
return this
}
if (start === 0) {
return this.prepend(str)
}
if (start === this.length) {
return this.append(str)
}
const index = MagicString.#splitData(start, this.data)
if (str instanceof MagicString) {
this.data.splice(index, 0, ...MagicString.#makeDataCopy(str.data))
} else {
this.data.splice(index, 0, MagicString.#convertStringToData(String(str)))
}
this.length += str.length
return this
}
split(offset) {
// Check for valid offset.
if ((offset < 0) || (offset > this.length)) {
throw new Error(`Invalid offset (${offset}) value`)
}
if (offset === 0) {
return [new MagicString(), this]
}
if (offset === this.length) {
return [this, new MagicString()]
}
return [
new MagicString(this).substring(0, offset),
new MagicString(this).substring(offset),
]
}
}
////// ------ TESTS ------ //////
;(() => {
function assertStringsEqual(str1, str2) {
if (String(str1) !== String(str2)) {
throw new Error(`str1 (${str1}) and str2 (${str2}) are not equivalent`)
}
}
const errors = []
;[
function testSingleStringConstructor() {
const str = 'hello'
assertStringsEqual(str, new MagicString(str))
},
function testMultiStringConstructor() {
const str1 = 'hello'
const str2 = 'world'
const str = str1 + str2
assertStringsEqual(str, new MagicString(str1, str2))
},
function testMultiStringSubstring() {
const str1 = 'hello'
const str2 = 'world'
const str = str1 + str2
const start = 3
const end = 7
assertStringsEqual(str.substring(start, end), new MagicString(str1, str2).substring(start, end))
},
function testMultiStringSplit() {
const str1 = 'hello'
const str2 = 'world'
const str = str1 + str2
const split = 3
const [magicString1, magicString2] = new MagicString(str1, str2).split(split)
assertStringsEqual(str.substring(0, split), magicString1)
assertStringsEqual(str.substring(split), magicString2)
},
].forEach((test) => {
try {
test()
} catch (err) {
errors.push(err)
}
})
if (errors.length) {
errors.forEach(console.error)
} else {
console.log('All tests passed successfully!')
}
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment