Text Parsing Patterns
IntermediateExtract, split, and transform text in M using Text.* functions — parsing delimited strings, extracting substrings, and cleaning messy data without regular expressions.
Splitting on a Delimiter
Text.Split returns a list of parts. Access specific positions with {index}:
let
Source = Customers,
// Split "Smith, John" into last and first name
WithLast = Table.AddColumn(Source, "LastName",
each Text.Trim(Text.Split([FullName], ","){0}), type text),
WithFirst = Table.AddColumn(WithLast, "FirstName",
each Text.Trim(Text.Split([FullName], ","){1}), type text)
in
WithFirstWhen the number of parts is variable, guard against index-out-of-bounds with try ... otherwise null:
Table.AddColumn(Source, "Extension",
each try Text.Split([Phone], "x"){1} otherwise null,
type nullable text)Extracting Between Delimiters
Text.BetweenDelimiters extracts text between a start and end marker without index arithmetic:
// Input: "Order [#12345] - Pending"
// Extract: "12345"
Table.AddColumn(Source, "OrderID",
each Text.BetweenDelimiters([Description], "[#", "]"),
type text)For repeated delimiters, use the optional count arguments to select which occurrence:
// Text: "2024/03/15"
// Extract the month (second segment, 0-indexed)
Text.BetweenDelimiters("2024/03/15", "/", "/", 0, 0)
// Result: "03"Extracting from the Start or End
let
Code = "AB-12345-XZ",
Prefix = Text.Start(Code, 2), // "AB"
Suffix = Text.End(Code, 2), // "XZ"
Middle = Text.Range(Code, 3, 5) // "12345" (start at index 3, take 5 chars)
in
{Prefix, Suffix, Middle}Finding a Position and Slicing
When the delimiter position varies, Text.PositionOf gives you the index to slice at:
// Extract domain from email: "user@example.com" → "example.com"
Table.AddColumn(Source, "Domain",
each
let pos = Text.PositionOf([Email], "@")
in if pos = -1 then null
else Text.Range([Email], pos + 1),
type nullable text)Text.PositionOf returns -1 when the character isn't found — always guard against it.
Cleaning and Normalizing Text
let
Source = Customers,
// Remove leading/trailing whitespace, then collapse internal spaces
Normalized = Table.TransformColumns(Source, {
{"Name", each Text.Trim(_), type text},
{"Email", each Text.Lower(Text.Trim(_)), type text},
{"Phone", each Text.Remove(_, {" ", "-", "(", ")"}), type text},
{"Country", each Text.Proper(Text.Trim(_)), type text}
})
in
NormalizedText.Remove strips every character in the list. Text.Select keeps only characters in the list — useful for extracting digits:
// Extract only digits from a messy phone number "(206) 555-1234"
Text.Select("(206) 555-1234", {"0".."9"})
// Result: "2065551234"Replacing Multiple Values
For more than one substitution, chain Text.Replace or use List.Accumulate:
let
Replacements = {
{"&", "&"},
{"<", "<"},
{">", ">"},
{" ", " "}
},
// Apply all replacements to a single value
DecodeHtml = (s as text) as text =>
List.Accumulate(Replacements, s, (state, pair) =>
Text.Replace(state, pair{0}, pair{1})
),
Decoded = Table.TransformColumns(Source, {{"HtmlContent", each DecodeHtml(_), type text}})
in
DecodedSplitting into Rows (Text → Table)
When one cell contains a newline-delimited list, expand it into multiple rows:
let
Source = Orders, // Each row has [OrderID] and [Tags] = "red\nurgent\nexport"
Expanded = Table.TransformColumns(Source, {
{"Tags", each Text.Split(_, "#(lf)"), type {type text}}
}),
Result = Table.ExpandListColumn(Expanded, "Tags")
in
Result"#(lf)" is M's escape sequence for a line feed character.