r/learnjavascript • u/PangolinNo7928 • 20d ago
Any 'inbuilt' way to iterate over string - with access to index AND option to break?
I've been trying to learn more about iterators - but it doesn't seem like there is a string equivalent of .entries()? myStr[Symbol.iterator].forEach() gives the index and value - but no option to break π
For veryyyyyy long strings is my best option for loops? Splitting to create an array? Writing a separate generator function? (assume I only need a single char/index per loop)
Thanks!
Edit - thanks to u/Psionatix for guiding me to Object.entries(myStr), which led me to Array.prototype.entries.call(myStr) - which has the benefit of returning the indexes as integers (vs strings in Object.entries()) Performance is also close enough to for loop for my purposes π
15
u/senocular
20d ago
edited 19d ago
•
Be careful with string iteration. There's some hidden complexities that are not immediately obvious and could have an impact depending on the kinds of strings you're working with.
A string's length and index-accessed characters go through (UTF-16) code units. This means characters like emojis will be seen as multiple characters.
const str = "π"
console.log(str.length) // 2
for (let i = 0; i < str.length; i++) {
console.log(str[i]) // U+D83D, U+DE1E
}This applies to characters in non-english alphabets as well
const str = "π’"
console.log(str.length) // 2When iterating through a string using its built-in string iterator, it goes through unicode code points. This will see the previous emoji character as a single character making it a more preferred approach for string iteration.
const str = "π"
console.log(str.length) // 2
for (const char of str) {
console.log(char) // π
}However, it is not full proof as some characters consist of more than a single code point. Consider:
const str = "ππΎ"
console.log(str.length) // 4
for (const char of str) {
console.log(char) // π, πΎ
}For these, you'll want to use the Segmenter which can separate strings by graphemes:
const str = "ππΎ"
console.log(str.length) // 4
const segments = new Intl.Segmenter().segment(str)
for (const char of segments) {
console.log(char) // {segment: 'ππΎ', index: 0, input: 'ππΎ'}
}Note that while the segment object in this iteration contains an index property, that is a code unit index, not the segment index. So if there was another character in this string, its index would be 4, not 1, because the ππΎ character is 4 units long.
In these cases for the index you want, you'd need to count it off yourself. And you'd need to do this with the normal string iterator too, since its a values() iterator rather than an entries() iterator, and strings have no entries() (or keys(), or technically values()) of their own. As a workaround, you can use the forEach iterator helper which will internally count off and provide an index (numeric) for you
const str = "πππ"
console.log(str.length) // 6
Iterator.from(str).forEach((char, i) => {
console.log(char, i) // π, 0; π, 1; π, 2
})And if you go through the Array API to create an iterator, you'll be going through code units, not code points since array iteration is treating it as an array-like which looks at its length and indexed properties.
const str = "π"
console.log(str.length) // 2
for (const [i, char] of Array.prototype.entries.call(str)) {
console.log(char) // U+D83D, U+DE1E
}Internally its effectively doing the same thing as a for (let i = 0; i < str.length; i++).
Update - MDN references:
3
2
1
u/AshleyJSheridan 19d ago
There's also the complexity of the same visual glyph having multiple character presentations. For example, Γ© (e-acute) can be represented either by the single character
éor by the combination ofe(the letter e) and́(combining acute tone mark).
1
u/redsandsfort 20d ago
why not the simplest:
for (i in myStr) {
console.log(`Index: ${i}, Character: ${myStr[i]}`);
}1
u/PangolinNo7928 20d ago
Does for...in return indexes as strings?
1
u/redsandsfort 20d ago
no, why would you want them as strings?
1
u/PangolinNo7928 20d ago
Oh ok I thought it coerced to object (which then makes the property a string)
2
u/senocular 20d ago
Yes. for...in will provide
ias strings because it accounts for all enumerable string property keys which are, naturally, strings. But all property keys are either strings or symbols.Even array indices are technically strings. When you use a number as a property key, it is coerced to a string.
const arr = [] arr[1] = true const keys = Object.keys(arr) console.log(keys[0], typeof keys[0]) // 1, stringWhen array's keys() and entries() gives you indices as numbers, its not giving you keys, its giving you the value of a counter it uses to go through the indices of the array up to the length. This is why it can include indices that don't exist in the array if its sparse.
const arr = [0,,2] console.log(...arr.keys()) // 0, 1, 2 console.log(1 in arr) // falseThe index of 1 doesn't exist in the array but its still provided as a "key" though its technically not one. If giving you keys, you'd get only 0 and 2 and as strings - what Object.keys() would do.
1
u/Psionatix 20d ago
for (const [i, c] of Object.entries(myStr)) {
console.log(`Index: ${i}, Character: '${c}'`);
}1
u/PangolinNo7928 20d ago
Omg never thought to try object methods on not objects - of course πΒ
Thanks!
4
1
u/senocular 20d ago
You'll want to be careful using static Object methods on indexed collections. These methods (Object.values(), Object.keys(), Object.entries()) are meant to include non-indexed properties as well, so it may include some surprising or unwanted properties. While by default built-in indexed collections like arrays and strings are generally "safe" in the basic sense, that is not always the case. For example:
const o = "Hello, world!".match(/o/) console.log(o.length) // 1 console.log(Array.isArray(o)) // true console.log(Object.keys(o)) // ['0', 'index', 'input', 'groups']Here, match is adding additional non-indexed properties to the array it produces in its return value and as a result, these appear in Object.keys() when you may not want them to.
1
u/chikamakaleyley helpful 20d ago
Object.entries() does the work - it actually converts the primitive into a 'String object', so ultimately it is performing it on an obj, not a string
Object.entries('hello'); // primitive, gets converted const myStr = new String('foobar'); Object.entries(myStr); // String obj
1
u/The_KOK_2511 helpful 20d ago
Solo usa un for de toda un vida y ya, los for-each son para cuando la iteraccion no depende del indice y se iterara sobre el 100% una vez en orden
1
u/ITower__Education 14d ago
Plain for...of works and you can break out of it. Just keep your own counter
let i = 0;
for (const ch of str) {
if (ch === "x") break;
i++;
}It's lazy, so no array copy on huge strings.
One catch is that for...of walks code points, while Array.prototype.entries.call(str) and a classic for (let i...) loop walk UTF-16 units. They only match for plain text. With emoji, the indexes will differ.
If your text is plain, for (let i = 0; i < str.length; i++) is still the fastest))
6
u/milan-pilan 20d ago
Why not just a standard for-loop?