Wicked Smart Data
LearnArticlesAbout
Sign InSign Up
LearnArticlesAboutContact
Sign InSign Up
Wicked Smart Data

The go-to platform for professionals who want to master data, automation, and AI — from Excel fundamentals to cutting-edge machine learning.

Platform

  • Learning Paths
  • Articles
  • About
  • Contact

Connect

  • Contact Us
  • RSS Feed

© 2026 Wicked Smart Data. All rights reserved.

Privacy PolicyTerms of Service
All Articles
Text Manipulation and String Functions in Power Query M: Parsing, Splitting, and Transforming Text Data

Text Manipulation and String Functions in Power Query M: Parsing, Splitting, and Transforming Text Data

Power Query🌱 Foundation15 min readJul 15, 2026Updated Jul 15, 2026
Table of Contents
  • Introduction
  • Prerequisites
  • How M's Text Functions Work
  • Cleaning and Standardizing Text
  • Removing Extra Spaces with Text.Trim and Text.Clean
  • Standardizing Capitalization
  • Extracting Substrings: Getting the Parts You Need

On this page

  • Introduction
  • Prerequisites
  • How M's Text Functions Work
  • Cleaning and Standardizing Text
  • Removing Extra Spaces with Text.Trim and Text.Clean
  • Standardizing Capitalization
  • Extracting Substrings: Getting the Parts You Need
  • Text.Start and Text.End: Taking from the Edges
  • Text.Middle and Text.Range: Taking from the Inside
  • Splitting Text: Turning One Column into Many
  • Text.Start and Text.End: Taking from the Edges
  • Text.Middle and Text.Range: Taking from the Inside
  • Splitting Text: Turning One Column into Many
  • Text.Split: The Core Splitter
  • Splitting by Length: Splitter.SplitTextByLengths
  • Searching Within Strings
  • Text.Contains, Text.StartsWith, Text.EndsWith
  • Text.PositionOf: Finding Where Something Is
  • Building and Modifying Strings
  • The & Operator: Simple Concatenation
  • Text.Combine: Joining a List with a Separator
  • Text.Replace: Swapping One String for Another
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • Summary & Next Steps
  • Text Manipulation and String Functions in Power Query M: Parsing, Splitting, and Transforming Text Data

    Introduction

    You've imported a dataset from your company's legacy CRM system. The customer names are all uppercase, phone numbers are formatted inconsistently with a mix of dashes, dots, and parentheses, and full addresses are crammed into a single column that someone concatenated together with pipe characters fifteen years ago. You need clean, structured data — but the source isn't going to change. This is your problem to solve.

    Text manipulation is where Power Query earns its keep in real-world data work. Raw data almost never arrives in the shape you need it. Strings — the technical term for sequences of text characters — are especially messy. Dates embedded in file names, product codes with meaningful segments baked in, email addresses that contain both the person's name and their department if you know how to look — the information is there, it just needs to be extracted.

    By the end of this lesson, you'll be able to parse, split, reformat, and transform text data using Power Query's M language functions with confidence. You won't just be clicking buttons in the GUI — you'll understand the underlying functions well enough to write and modify them by hand, which opens up a whole class of transformations that the ribbon simply can't do.

    What you'll learn:

    • How M's Text.* function library is organized and how to read its syntax
    • How to extract substrings using Text.Start, Text.End, Text.Middle, and Text.Range
    • How to split text with Text.Split and Splitter functions, and when to use each
    • How to clean and standardize text with Text.Trim, Text.Clean, Text.Upper, Text.Lower, and Text.Proper
    • How to search within strings using Text.Contains, Text.StartsWith, Text.EndsWith, and Text.PositionOf
    • How to build and combine strings with Text.Combine, Text.Insert, and Text.Replace

    Prerequisites

    This lesson is marked as Foundation-level within the Advanced M Language path, which means you should:

    • Know how to open Power Query Editor in Excel or Power BI (Home tab → Transform Data, or Data → Get Data)
    • Be comfortable reading a basic M query — you've seen let and in before and they don't frighten you
    • Understand what a column transformation step looks like in the Applied Steps pane

    You do not need to have written M functions from scratch before. We'll build that skill here.


    How M's Text Functions Work

    Before we touch any data, let's establish the mental model. M is a functional language, which means you do everything by calling functions. The Text library is a collection of functions that all start with Text. followed by a descriptive name — Text.Upper, Text.Split, Text.Replace, and so on.

    Every function takes inputs (called arguments) and returns an output. Arguments go inside parentheses, separated by commas. The first argument is almost always the text you want to work with:

    Text.Upper("hello world")
    // Returns: "HELLO WORLD"
    

    You can test any function immediately by opening Power Query, creating a blank query (Home → New Source → Blank Query), and typing your expression directly into the formula bar after = . This is your playground. Use it.

    One important thing to know: M is case-sensitive for function names. text.upper will throw an error. Text.Upper will work. This trips up beginners constantly.

    Tip: If you're not sure what arguments a function accepts, you can type the function name with an opening parenthesis in the formula bar — Text.Replace( — and Power Query will often show you a tooltip. You can also check Microsoft's official M function reference, but the tooltip is faster.


    Cleaning and Standardizing Text

    Let's start with the most common task: cleaning up messy text. You've received a list of customer names that look like this:

    "  ACME CORPORATION   "
    "widget  co."
    "  Northern Lights LLC"
    

    Inconsistent capitalization, leading and trailing spaces, internal double spaces — a mess. Here's how to fix each problem.

    Removing Extra Spaces with Text.Trim and Text.Clean

    Text.Trim removes leading and trailing whitespace (spaces at the beginning and end of a string). It does not touch spaces in the middle:

    Text.Trim("  ACME CORPORATION   ")
    // Returns: "ACME CORPORATION"
    

    Text.Clean does something different and often misunderstood — it removes non-printable characters (invisible control characters like line feeds or carriage returns that sometimes sneak into data exported from other systems). It does not remove visible spaces:

    Text.Clean("ACME" & Character.FromNumber(10) & "CORPORATION")
    // Returns: "ACMECORPORATION"
    // (The line feed character between the words is removed)
    

    In practice you often want to chain both together:

    Text.Trim(Text.Clean("  ACME CORPORATION   "))
    

    Read this inside-out: Text.Clean runs first, removing invisible characters, then Text.Trim removes the surrounding spaces. This pattern handles almost every whitespace-related cleaning problem.

    Standardizing Capitalization

    Three functions, clear purposes:

    Text.Upper("acme corporation")   // Returns: "ACME CORPORATION"
    Text.Lower("ACME CORPORATION")   // Returns: "acme corporation"
    Text.Proper("acme corporation")  // Returns: "Acme Corporation"
    

    Text.Proper capitalizes the first letter of each word and lowercases everything else. It's your go-to for turning all-caps customer names into readable format. Be aware it has one quirk: it treats any non-letter character as a word boundary, so "O'BRIEN" becomes "O'Brien" — which is actually correct behavior, though it can occasionally surprise you with hyphenated names or abbreviations.

    In a real query, you'd apply this to a column named [CustomerName] in a transformation step:

    TransformedNames = Table.TransformColumns(
        PreviousStep,
        {{"CustomerName", each Text.Proper(Text.Trim(Text.Clean(_))), type text}}
    )
    

    The _ is M's shorthand for "the current value" inside an each expression. You'll see it constantly.


    Extracting Substrings: Getting the Parts You Need

    Extraction is where things get interesting. You have a string and you need to pull out a specific portion of it.

    Text.Start and Text.End: Taking from the Edges

    Text.Start(text, count) returns the first count characters from a string. Text.End(text, count) returns the last count characters.

    Imagine you have product codes formatted like "PRD-20240315-WEST". The first three characters are always the product type prefix:

    Text.Start("PRD-20240315-WEST", 3)
    // Returns: "PRD"
    

    And the last four are always the region code:

    Text.End("PRD-20240315-WEST", 4)
    // Returns: "WEST"
    

    Text.Middle and Text.Range: Taking from the Inside

    Sometimes the piece you want is buried in the middle. Text.Middle(text, offset, count) starts at position offset (counting from zero!) and returns count characters.

    From the same product code, extracting the date portion "20240315" — which starts at position 4 (after "PRD-") and is 8 characters long:

    Text.Middle("PRD-20240315-WEST", 4, 8)
    // Returns: "20240315"
    

    Warning: M counts character positions starting from zero, not one. Position 0 is the first character. This is the single most common source of off-by-one errors in string extraction. When you're calculating your offset, always mentally number the characters 0, 1, 2... rather than 1, 2, 3...

    Text.Range is essentially identical to Text.Middle — same arguments, same behavior. It exists for historical reasons. Prefer Text.Middle for clarity.

    If you omit the count argument from Text.Middle, it returns everything from the offset to the end of the string:

    Text.Middle("PRD-20240315-WEST", 4)
    // Returns: "20240315-WEST"
    

    Splitting Text: Turning One Column into Many

    The scenario: an address column contains "742 Evergreen Terrace | Springfield | IL | 62701". You need four separate columns. This is Text.Split's job.

    Text.Split: The Core Splitter

    Text.Split(text, separator) divides a string at every occurrence of the separator and returns a list of the resulting pieces:

    Text.Split("742 Evergreen Terrace | Springfield | IL | 62701", " | ")
    // Returns: {"742 Evergreen Terrace", "Springfield", "IL", "62701"}
    

    The curly braces {} denote a list in M. To get a specific item from the list, use {index} notation (again, zero-based):

    Text.Split("742 Evergreen Terrace | Springfield | IL | 62701", " | "){0}
    // Returns: "742 Evergreen Terrace"
    
    Text.Split("742 Evergreen Terrace | Springfield | IL | 62701", " | "){2}
    // Returns: "IL"
    

    In a full table transformation, you'd typically use Table.SplitColumn or add custom columns for each part. Here's how to add a "State" column by pulling position 2 from the split:

    AddStateColumn = Table.AddColumn(
        PreviousStep,
        "State",
        each Text.Split([FullAddress], " | "){2},
        type text
    )
    

    Splitting by Length: Splitter.SplitTextByLengths

    Sometimes data has no delimiter — it's fixed-width, meaning each field occupies a specific number of characters. Bank transaction exports and old mainframe dumps often look like this.

    Imagine account codes formatted as "CHK00487291" where the first three characters are account type, the next five are branch code, and the last six are account number. Splitter.SplitTextByLengths handles this:

    Splitter.SplitTextByLengths({3, 5, 6})("CHK00487291")
    // Returns: {"CHK", "00487", "291"} ... wait, that's only 8 chars for 5+6
    // Correct example:
    Splitter.SplitTextByLengths({3, 5, 6})("CHK0048729100")
    // Returns: {"CHK", "00487", "291001"} -- hmm, let's be precise:
    // "CHK" = 3, "00487" = 5, "291001" doesn't exist
    // Better: "CHK004872910001" — just use a realistic total
    

    Let's use a cleaner example to avoid confusion. Account code "CHK004871" — 3 chars type, 3 chars branch, 3 chars number:

    Splitter.SplitTextByLengths({3, 3, 3})("CHK004871")
    // Returns: {"CHK", "004", "871"}
    

    The Splitter.* family of functions returns a function — notice the double parentheses. The first call configures the splitter; the second call applies it to a specific string. This is called a higher-order function, and while it looks strange at first, it makes these functions composable and reusable.

    Tip: In the Power Query GUI, when you right-click a column and choose "Split Column," you're actually generating Splitter function calls behind the scenes. After using the GUI, click on the generated step in Applied Steps and look at the formula bar — you'll see exactly which Splitter function was called. This is a great way to learn.


    Searching Within Strings

    Before you transform text, you often need to ask questions about it: Does this string contain a keyword? Where does a certain character appear?

    Text.Contains, Text.StartsWith, Text.EndsWith

    These three return true or false and are most useful as conditions in filters or conditional columns:

    Text.Contains("support@company.com", "@")       // true
    Text.StartsWith("INV-2024-0042", "INV")         // true
    Text.EndsWith("report_final_FINAL.xlsx", ".xlsx") // true
    

    A practical use case: you need to flag all invoice numbers that start with "INV" and filter out the rest. In a custom column:

    [DocumentType] = if Text.StartsWith([DocumentNumber], "INV") 
                     then "Invoice" 
                     else "Other"
    

    Text.PositionOf: Finding Where Something Is

    Text.PositionOf(text, substring) returns the zero-based position of the first occurrence of substring within text. If the substring isn't found, it returns -1.

    Why is this useful? Because you can use the position of a known character to make dynamic extractions. Consider email addresses like "jsmith@northernlights.com". The position of "@" tells you exactly where the username ends:

    Text.PositionOf("jsmith@northernlights.com", "@")
    // Returns: 6
    

    Now you can extract the username dynamically — it works regardless of how long the username is:

    let
        email = "jsmith@northernlights.com",
        atPosition = Text.PositionOf(email, "@"),
        username = Text.Start(email, atPosition)
    in
        username
    // Returns: "jsmith"
    

    And the domain:

    let
        email = "jsmith@northernlights.com",
        atPosition = Text.PositionOf(email, "@"),
        domain = Text.Middle(email, atPosition + 1)
    in
        domain
    // Returns: "northernlights.com"
    

    The +1 skips past the @ character itself. This combination of Text.PositionOf with extraction functions is one of the most powerful patterns in text parsing.


    Building and Modifying Strings

    Sometimes you need to go the other direction — constructing new strings or modifying existing ones.

    The & Operator: Simple Concatenation

    The & operator joins strings end-to-end:

    "Invoice-" & "2024" & "-" & "0042"
    // Returns: "Invoice-2024-0042"
    

    Note that both sides of & must be text. If you're concatenating a number, convert it first with Text.From:

    "Row count: " & Text.From(1547)
    // Returns: "Row count: 1547"
    

    Text.Combine: Joining a List with a Separator

    If you need to join multiple values with a consistent separator — like rebuilding a CSV field or constructing a comma-separated list — Text.Combine is cleaner than chaining & operators:

    Text.Combine({"Alice", "Bob", "Carol"}, ", ")
    // Returns: "Alice, Bob, Carol"
    

    This pairs naturally with Text.Split for round-trip transformations: split to process individual pieces, then combine to reassemble.

    Text.Replace: Swapping One String for Another

    Text.Replace(text, old, new) replaces every occurrence of old with new:

    Text.Replace("(555) 867-5309", "(", "")
    // Returns: "555) 867-5309"
    

    To strip all non-numeric characters from a phone number, you'd chain multiple replacements:

    let
        raw = "(555) 867-5309",
        step1 = Text.Replace(raw, "(", ""),
        step2 = Text.Replace(step1, ")", ""),
        step3 = Text.Replace(step2, "-", ""),
        step4 = Text.Replace(step3, " ", "")
    in
        step4
    // Returns: "5558675309"
    

    Hands-On Exercise

    Work through this exercise in Power Query using a blank query or by adding custom columns to any table.

    Scenario: You've received an export of employee records from an old HR system. The EmployeeCode column contains values formatted like:

    "ENG-042-REMOTE-jdoe@company.com"

    The structure is always: Department (3 chars) - EmployeeID (3 chars) - WorkType - Email

    Your tasks:

    1. Extract the department code (the first 3 characters before the first dash).

    2. Extract the employee ID (the 3 characters after the first dash).

    3. Extract the email address (everything after the last dash — but here, use Text.Split to split on "-" and take the last element. Be careful: the email itself contains no dashes in this dataset).

    4. Extract the username from the email address (everything before @).

    5. Create a display name by combining the department code and employee ID with a space: "ENG 042".

    Sample solution approach:

    let
        raw = "ENG-042-REMOTE-jdoe@company.com",
        
        // Task 1: Department code
        Department = Text.Start(raw, 3),
        
        // Task 2: Employee ID
        EmployeeID = Text.Middle(raw, 4, 3),
        
        // Task 3: Email — split by "-" and take last element
        Parts = Text.Split(raw, "-"),
        Email = List.Last(Parts),
        
        // Task 4: Username from email
        AtPos = Text.PositionOf(Email, "@"),
        Username = Text.Start(Email, AtPos),
        
        // Task 5: Display name
        DisplayName = Department & " " & EmployeeID
        
    in
        DisplayName
    

    Try each step individually by changing what comes after in. Make sure you get "ENG", "042", "jdoe@company.com", "jdoe", and "ENG 042" at the respective steps.


    Common Mistakes & Troubleshooting

    "Expression.Error: We cannot convert the value null to type Text." You're applying a text function to a column that contains null values. Wrap your expression in a null check: if [Column] = null then null else Text.Upper([Column]). This is tedious but necessary. Alternatively, replace nulls before the text transformation step.

    Off-by-one errors in position calculations. You expected Text.Middle("hello", 1, 3) to return "hel" but got "ell". Remember: position 0 is h, position 1 is e. Always count from zero. When debugging, use Text.PositionOf to verify positions rather than counting manually.

    Text.Split gives you more pieces than expected. If your delimiter appears in unexpected places — like a comma inside a quoted field in CSV-like data — you'll get extra list items. Either pre-clean the data or use a more specific delimiter. If you're working with actual CSV parsing, use the built-in CSV source connector rather than splitting manually.

    Forgetting that Splitter.* functions return functions. If you write Splitter.SplitTextByDelimiter("-") without a second set of parentheses with the actual string, you'll get a function back, not a result. You need Splitter.SplitTextByDelimiter("-")("my-string-here").

    Case sensitivity in comparisons. Text.Contains("Hello", "hello") returns false by default because M string comparisons are case-sensitive. Use Text.Contains("Hello", "hello", Comparer.OrdinalIgnoreCase) if you need case-insensitive matching. The same optional Comparer argument applies to Text.StartsWith, Text.EndsWith, and Text.PositionOf.


    Summary & Next Steps

    You've covered the full lifecycle of text data in M — from cleaning and standardizing raw input, to extracting meaningful pieces, to building new strings from components. The core functions to commit to memory:

    • Clean up: Text.Trim, Text.Clean, Text.Upper, Text.Lower, Text.Proper
    • Extract pieces: Text.Start, Text.End, Text.Middle, Text.PositionOf
    • Split: Text.Split, Splitter.SplitTextByLengths, Splitter.SplitTextByDelimiter
    • Search: Text.Contains, Text.StartsWith, Text.EndsWith
    • Build: &, Text.Combine, Text.Replace

    The most important skill you've practiced isn't any individual function — it's composing these functions together. Text.Start becomes genuinely powerful when you feed it a dynamic length calculated by Text.PositionOf. Text.Split becomes essential when you pair it with List.Last to grab the final segment. Think in pipelines.

    Where to go next:

    • Regular expressions via Text.RegEx — M doesn't have native regex, but Power Query supports it through Text.RegEx in certain environments, and you can implement pattern matching logic using the functions you learned here
    • List functions — Since Text.Split returns lists, learning List.First, List.Last, List.Select, and List.Transform will dramatically expand what you can do with split text
    • Error handling with try...otherwise — Production-grade text parsing needs to handle unexpected input gracefully, and this construct is how you do it in M
    • Type conversion — Once you've extracted a date string like "20240315", you need Date.FromText or DateTime.FromText to make it usable as an actual date type

    Text manipulation is the unglamorous foundation of almost every real data project. Master it here, and the more complex M topics ahead will feel much more approachable.

    Learning Path: Advanced M Language

    Previous

    Implementing Row-Level Security and Dynamic Data Filtering Pipelines in Power Query M Using Parameter-Driven Permission Tables

    Next

    Building Multi-Stage ETL Pipelines in Power Query M: Orchestrating Dependent Transformations with Modular Query Chains

    Related Articles

    Power Query⚡ Practitioner

    Building a Dynamic Date Dimension Generator in Power Query M: Fiscal Periods, ISO Weeks, and Holiday Logic

    23 min
    Power Query⚡ Practitioner

    Generating Cross-Tabulated Reports in Power Query: Pivot, Aggregate, and Shape Data for Presentation

    19 min
    Power Query🌱 Foundation

    Working with Binary Data in Power Query M: Reading, Parsing, and Converting Bytes to Structured Tables

    16 min
  • Text.Split: The Core Splitter
  • Splitting by Length: Splitter.SplitTextByLengths
  • Searching Within Strings
  • Text.Contains, Text.StartsWith, Text.EndsWith
  • Text.PositionOf: Finding Where Something Is
  • Building and Modifying Strings
  • The & Operator: Simple Concatenation
  • Text.Combine: Joining a List with a Separator
  • Text.Replace: Swapping One String for Another
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • Summary & Next Steps