Last active
December 6, 2023 04:43
-
-
Save ZeikJT/6089265e5eda6c00c4c0038f7d14b5b7 to your computer and use it in GitHub Desktop.
Concatenating and substringing strings without doing native string operations in JS
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
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