Strings and text processing · Lesson 39 of 77 · practice
Splitting, joining and cleaning up
About 55 minutes.
By the end of this lesson you can
- Turn one string into an array of pieces with split, choosing a separator that suits the data.
- Turn an array of pieces back into one string with join, choosing what goes between them.
- Strip the invisible whitespace users leave behind with trim, trimStart and trimEnd.
- Predict split's edge cases: an empty string, an empty separator, and two separators in a row.
- Write the clean-up pipeline — split, trim each piece, drop the empties — without looking it up.
Text arrives dirty
Why it matters
Text you did not write is messy in predictable ways. Someone types "red, green ,, blue ," into a tags box: a space after some commas and not others, an extra comma where they changed their mind, a trailing comma because they were about to add a fourth. Someone else pastes a name from a spreadsheet and brings a tab and a line break with it.
None of that is user error. It is what real input looks like, and code that only works on tidy input is code that only works in the demo. This lesson is the standard tool kit for turning messy text into clean data: cut it into pieces, tidy each piece, throw away the ones that are empty, and — when you need text again — glue the survivors back together.
split: one string becomes many
Explanation
`text.split(separator)` gives you an ARRAY of pieces, cutting at every occurrence of the separator and throwing the separator itself away. `"a,b,c".split(",")` gives `["a", "b", "c"]`.
The original string is untouched, as always. What you get back is a new array, and from that point on you are working with arrays — every tool from module 5 applies, which is the real reason split is so useful.
Two special separators are worth knowing. `split("")` — an empty separator — cuts between every character, so `"abc".split("")` gives `["a", "b", "c"]`. And `split(" ")` for words is a common first attempt that goes wrong the moment there are two spaces in a row; the regular-expression lesson gives you a better tool for that.
split also takes an optional second argument, a limit on how many pieces you want back: `"a,b,c".split(",", 2)` gives `["a", "b"]`. It simply stops after that many; the remaining text is not appended anywhere, so use it only when you genuinely want the rest discarded.
join: many become one
Explanation
`list.join(separator)` is the mirror image. It converts each item to text and glues the results together with your separator in between: `["red", "green"].join(" | ")` gives `"red | green"`.
Watch the default. Calling `join()` with no argument uses a comma, NOT a space and not an empty string — `["a", "b"].join()` is `"a,b"`. If you want the pieces run together, ask for it explicitly with `join("")`.
join is a method on arrays, not on strings, which is the giveaway that you have crossed back over: split takes you from text to data, join takes you from data to text. Most text-processing jobs are exactly that round trip with some array work in the middle.
trim: the whitespace you cannot see
Explanation
`text.trim()` returns a new string with whitespace removed from BOTH ends. Whitespace means spaces, tabs and line breaks — everything the user cannot see and did not mean to type.
`trimStart()` and `trimEnd()` do one end each, for the rarer cases where the leading indentation is meaningful or the trailing newline is.
The crucial limit: trim only touches the ENDS. `"a b".trim()` is still `"a b"` — the three spaces in the middle are part of the text, and squeezing those out is a different job needing a regular expression.
Trim early, at the boundary where text enters your program, and you will not have to wonder later whether a value has a stray space on it. An untrimmed value is why `"ada " === "ada"` is false and why the user swears they typed the right thing.
The edge cases that produce empty pieces
A common mistake
split never merges separators, and this surprises everyone once. `"a,,b".split(",")` gives THREE pieces — `["a", "", "b"]` — because there genuinely is nothing between the two commas, and split reports what it finds rather than what you hoped for.
The same rule explains the other two: a trailing separator produces an empty piece at the end, and `"".split(",")` gives `[""]`, an array holding ONE empty string, not an empty array. That last one catches people who assume "no input means no pieces", and it is why a naive tag parser reports one blank tag for an empty box.
The fix is not to fight split. Let it produce the empty pieces and then remove them: `.filter((piece) => piece !== "")` after trimming. Trim first, or a piece containing only spaces will survive the filter.
The pipeline is always the same four steps
A mental model
Split into pieces. Trim each piece with `.map()`. Drop the empties with `.filter()`. Do whatever the job actually needs. Then, if you need text back, join.
It is worth committing that order to memory, because the order matters: filtering before trimming leaves pieces that are nothing but spaces, and joining before filtering leaves you with a doubled separator in the output.
Everything in the middle is ordinary array work from module 5 — the same `map`, `filter` and `reduce` you already know. Text processing is not a separate skill; it is array work with a split at the front and a join at the back.
What to take away
Recap
split(sep) makes an array, join(sep) makes a string, and join() with no argument uses a comma. trim() strips both ends only, never the middle.
split reports empty pieces honestly: "a,,b" gives three, and "" gives [""]. Handle them with trim then filter, in that order.
Worked examples
Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.
The whole pipeline on realistic input
One messy tag string taken through every stage, printing the array after each one. Watch the empty pieces appear at step one and disappear at step three — that is the part people leave out and then debug for an hour.
1const raw = " red, green ,, blue , ";2const parts = raw.split(",");3console.log(JSON.stringify(parts));4const trimmed = parts.map((part) => part.trim());5console.log(JSON.stringify(trimmed));6const cleaned = trimmed.filter((part) => part !== "");7console.log(JSON.stringify(cleaned));8console.log(cleaned.join(" | "));- line 3
Five pieces, and the spaces are still attached. The empty one came from the two commas in a row; the last is two spaces after the trailing comma.
- line 5
Trimming turns the spaces-only piece into a genuinely empty string, which is what makes the next step work.
- line 7
Now the filter can catch both empties with one simple test. Trim first, filter second — the other order does not work.
- line 8
join puts the separator BETWEEN items only, so there is no leading or trailing pipe.
What it prints
[" red"," green ",""," blue "," "]["red","green","","blue",""]["red","green","blue"]red | green | blue
The five split results people guess wrong
Each line is a case that has cost somebody an afternoon. JSON.stringify is used so the empty strings are visible — printing an array of empties directly would look like nothing at all.
1console.log(JSON.stringify("".split(",")));2console.log(JSON.stringify("a,,b".split(",")));3console.log(JSON.stringify("a,b,".split(",")));4console.log(JSON.stringify("abc".split("")));5console.log(JSON.stringify("a,b,c".split(",", 2)));- line 1
An array containing ONE empty string, not an empty array. Splitting nothing still gives you one piece.
- line 2
Three pieces: there really is nothing between the two commas.
- line 3
A trailing separator produces a trailing empty piece.
- line 4
An empty separator cuts between every character.
- line 5
The limit stops split after two pieces and discards the rest — the c is gone, not appended.
What it prints
[""]["a","","b"]["a","b",""]["a","b","c"]["a","b"]
What trim does and does not remove
One string carrying a line break, spaces and a tab, printed with JSON.stringify so you can actually see what was removed. The last line is the limit to remember: the middle is never touched.
1const messy = "\n Ada Lovelace \t ";2console.log(JSON.stringify(messy));3console.log(JSON.stringify(messy.trim()));4console.log(JSON.stringify(messy.trimStart()));5console.log(JSON.stringify(messy.trimEnd()));6console.log(JSON.stringify("a b".trim()));- line 1
\n is a line break and \t is a tab — one character each, both invisible when printed normally.
- line 3
trim removes whitespace from both ends: the line break, the spaces and the tab all go.
- line 4
trimStart cleans the front and leaves the tail exactly as it was.
- line 6
The interior spaces survive. trim is about ends, not about tidiness.
What it prints
"\n Ada Lovelace \t ""Ada Lovelace""Ada Lovelace \t ""\n Ada Lovelace""a b"
Check yourself
Not scored, not stored. Getting one wrong is the useful part.
Three stages of the same pipeline. Write down all three answers before running it — the first number is the one most people get wrong.
const raw = "a, b,, c";const parts = raw.split(",");console.log(parts.length);console.log(JSON.stringify(parts.map((part) => part.trim())));console.log(JSON.stringify(parts.map((part) => part.trim()).filter((part) => part !== "")));
What does `"".split(",")` return?
What does `" a b ".trim()` return?
Write it yourself
Exercise · core · 7 tests
Clean up a list of tags
A tags box gives you one string of comma-separated tags typed by a human. Write `cleanTagList(raw)` that turns it into a tidy array. The rules: cut on commas, remove the spaces around each tag, lower-case each tag, throw away any tag that is left empty, and remove duplicates — keeping the FIRST time each tag appeared. `cleanTagList(" Red, green ,, BLUE ,red ")` gives `["red", "green", "blue"]`. The order of the array is part of the contract: tags come out in the order they first appeared, NOT sorted. The tests compare the array exactly, so a sorted answer will fail even though it contains the right tags.
Returns an array of lower-cased, trimmed, non-empty tags in first-appearance order with no duplicates. An empty string, or a string of only spaces and commas, returns an empty array. The array order is part of the contract and is compared exactly.
Define cleanTagList at the top level of your code — the tests call it by name.
Exercise · stretch · 7 tests
Parse a settings block into an object
You are given a block of text, one setting per line, in the form `key = value`. Write `parseSettings(block)` that turns it into an object. `parseSettings("name = Ada\nrole=engineer")` gives `{ name: "Ada", role: "engineer" }`. The rules: - lines are separated by `\n`; - spaces around the key and around the value are removed; - a blank line is skipped; - a line whose first non-space character is `#` is a comment and is skipped; - a line with no `=` at all is skipped; - only the FIRST `=` divides the line, so `url = https://a.test/?x=1` keeps `?x=1` in its value; - if the same key appears twice, the later line wins.
Returns a plain object of trimmed key/value strings. Blank lines, comment lines beginning with # and lines with no = are ignored. Splitting happens at the first = only, so values may contain further = characters. A later line with the same key replaces the earlier value. An empty block returns an empty object. Keys appear in first-appearance order, which the exact comparison depends on.
Define parseSettings at the top level of your code — the tests call it by name.
Worth remembering
- How many pieces does "a,,b".split(",") produce, and why?
Three: ["a", "", "b"]. split never merges adjacent separators — there really is nothing between the two commas. Likewise "".split(",") gives [""], one empty piece, not an empty array.
- What is the standard clean-up pipeline for messy delimited text?
split on the separator, .map(piece => piece.trim()), .filter(piece => piece !== ""), then do the work, then join if you need text back. Trim BEFORE filter, or spaces-only pieces survive.
- What does ["a", "b"].join() give you?
"a,b". The default separator is a comma, not a space and not the empty string. Ask for join("") if you want the pieces run together.
Check this lesson against the source
We wrote the explanation; we did not invent the facts. This is the page that backs them.
MDN — String.prototype.split()
Check there: split returns an array, accepts an optional limit, produces empty strings for adjacent or trailing separators, and returns an array containing one empty string when called on an empty string.
Check there: join uses a comma when called with no argument, and places the separator between items only.
Check there: trim removes whitespace from both ends only and returns a new string, leaving the original unchanged.
MDN — String.prototype.trimStart()
Check there: trimStart removes whitespace from the beginning of the string only.
Prefer to read the source in a structured breakdown? Turn this doc into a breakdown