📝

Funciones de Texto

FORMAT, CONCATENATE, LEFT, RIGHT, SUBSTITUTE, CONTAINSSTRING y todas las herramientas para manejar texto en DAX

Principiante Intermedio
🔍

Texto en DAX — cosas que debes saber 🎯

El texto en DAX puede ser un poco traicionero si no entiendes cómo funciona. Antes de lanzarnos a las funciones, hay dos cositas importantes que debes saber para evitar sorpresas. 🧐

Cosa 1 — DAX distingue BLANK de cadena vacía

// ⚠️ BLANK() no es lo mismo que ""
ISBLANK("")          -- FALSE (una cadena vacía no es BLANK)
ISBLANK(BLANK())     -- TRUE

// Muchas funciones de texto devuelven "" cuando no hay resultado
// y otras devuelven BLANK() — ¡verifica cuál usa cada función!

Cosa 2 — El operador & para concatenar

// En DAX, & concatena textos igual que en Excel
"Hola" & " " & "mundo"  -- "Hola mundo"
// Mucho más legible que CONCATENATE anidados

Cosa 3 — Las funciones de texto y case-sensitivity

// FIND y SEARCH son diferentes en este aspecto
FIND("abc", "ABC")   -- ERROR (FIND sí distingue mayúsculas)
SEARCH("abc", "ABC") -- 1 (SEARCH no distingue mayúsculas)
✂️

Extraer partes de un texto

LEFT / RIGHT / MID
Principiante
Extraen N caracteres desde el inicio, el final o una posición específica del texto.
// ✅ Código de país (primeros 2 caracteres del código de producto)
'Productos'[País] = LEFT('Productos'[CodProducto], 2)
// "ES-001" → "ES"

// ✅ Últimos 3 dígitos (sufijo)
'Productos'[Sufijo] = RIGHT('Productos'[CodProducto], 3)
// "ES-001" → "001"

// ✅ Del carácter 4 al 7 (MID: posición 1-based, no 0-based)
'Productos'[Segmento] = MID('Productos'[CodProducto], 4, 3)
// "ES-PREM-001" → "PRE"

// ✅ Extraer el dominio de un email
Dominio =
VAR PosArroba = FIND("@", 'Contactos'[Email])
RETURN
    MID('Contactos'[Email], PosArroba + 1, LEN('Contactos'[Email]) - PosArroba)
// "nombre@empresa.com" → "empresa.com"
LEN
Principiante
Devuelve el número de caracteres de un texto (incluyendo espacios).
// ✅ Verificar que los códigos tienen exactamente 8 caracteres
Codigo Valido = IF(LEN('Productos'[Codigo]) = 8, TRUE, FALSE)

// ✅ Detectar descripciones demasiado largas (para truncar)
Descripcion Display =
IF(
    LEN('Productos'[Descripcion]) > 100,
    LEFT('Productos'[Descripcion], 97) & "...",
    'Productos'[Descripcion]
)
🔍

Buscar dentro de texto

FIND / SEARCH
Intermedio
FIND: case-SENSITIVE (distingue mayúsculas). Da error si no encuentra, a menos que especifiques el 4º parámetro.
SEARCH: case-INSENSITIVE (no distingue mayúsculas). Igual con el parámetro si no encuentra.
Ambas devuelven la posición (1-based) donde empieza la coincidencia.

Tabla comparativa FIND vs SEARCH

Característica FIND SEARCH
Case sensitive ✅ Sí ❌ No
Wildcards (* ?) ❌ No ✅ Sí
Si no encuentra Error (a menos que se especifique valor) Error (idem)
Uso recomendado Búsquedas exactas Búsquedas flexibles
// ✅ Encontrar la posición del guión en un código
Pos Guion = FIND("-", 'Productos'[Codigo], 1, 0)
// "ES-001" → 3 (el guión está en posición 3)
// Si no hay guión → 0 (valor alternativo especificado)

// ✅ Extraer todo antes del primer guión (código de país)
Pais = LEFT('Productos'[Codigo], FIND("-", 'Productos'[Codigo], 1, 0) - 1)

// ✅ SEARCH con wildcard: buscar textos que contengan "promo" en cualquier posición
Tiene Promo = IF(SEARCH("promo*", 'Campañas'[Nombre], 1, 0) > 0, TRUE, FALSE)
CONTAINSSTRING / CONTAINSSTRINGEXACT
Principiante
Devuelven TRUE/FALSE indicando si el texto_buscar está contenido en texto_donde.
CONTAINSSTRING: case-insensitive
CONTAINSSTRINGEXACT: case-sensitive
Mucho más legibles que usar SEARCH > 0.
// ✅ ¿El nombre del producto contiene "premium"? (sin importar mayúsculas)
Es Premium = CONTAINSSTRING('Productos'[Nombre], "premium")
// "PREMIUM Gold" → TRUE
// "Premium Silver" → TRUE
// "Básico" → FALSE

// ✅ ¿El comentario contiene palabras clave negativas?
Requiere Atencion =
OR(
    CONTAINSSTRING('Tickets'[Comentario], "urgente"),
    CONTAINSSTRING('Tickets'[Comentario], "crítico"),
    CONTAINSSTRING('Tickets'[Comentario], "error grave")
)

// ✅ Con exactitud de mayúsculas
Es Codigo Sistema = CONTAINSSTRINGEXACT('Logs'[Tipo], "ERROR")
// "ERROR" → TRUE, "error" → FALSE
EXACT
Intermedio
Compara dos textos de forma EXACTA (case-sensitive). Devuelve TRUE solo si son idénticos incluyendo mayúsculas. A diferencia del operador =, que no distingue mayúsculas.
// = no distingue mayúsculas
"ABC" = "abc"        -- TRUE  (¡sorpresa para quien viene de otros lenguajes!)
EXACT("ABC", "abc")  -- FALSE (comparación exacta)

// ✅ Útil para validar que los códigos respetan el formato exacto
Codigo Correcto = EXACT('Productos'[Codigo], UPPER('Productos'[Codigo]))
// Solo TRUE si el código está completamente en mayúsculas
🔄

Transformar y limpiar texto

UPPER / LOWER
Principiante
Convierten todo el texto a mayúsculas o minúsculas. Muy útiles para normalizar datos antes de comparar o clasificar.
// ✅ Comparar independientemente de cómo el usuario escribió el país
Es España = LOWER(TRIM('Clientes'[Pais])) = "españa"
// Funciona con "ESPAÑA", "españa", " España ", "ESPAÑA "...
TRIM
Principiante
Elimina espacios del inicio y del final, y reduce múltiples espacios internos a uno solo. Fundamental para limpiar datos importados.
// ✅ Limpiar nombres con espacios extra
Nombre Limpio = TRIM('Clientes'[Nombre])
// "  Juan  García  " → "Juan García"

// ✅ Patrón de limpieza completo
Nombre Normalizado = UPPER(TRIM('Clientes'[Nombre]))
// "  juan garcía  " → "JUAN GARCÍA"
SUBSTITUTE
Principiante
Reemplaza TODAS las apariciones de un texto por otro (o una instancia específica si se indica). Case-sensitive.
// ✅ Eliminar espacios (reemplazar " " por "")
Sin Espacios = SUBSTITUTE('Productos'[Codigo], " ", "")
// "ES 001 A" → "ES001A"

// ✅ Reemplazar separador decimal para localización
Precio EU = SUBSTITUTE(FORMAT([Precio], "#,##0.00"), ".", ",")
// "1,234.56" → "1,234,56" (formato europeo)

// ✅ Limpiar caracteres no deseados
Telefono Limpio =
SUBSTITUTE(
    SUBSTITUTE(
        SUBSTITUTE('Clientes'[Telefono], "-", ""),
        " ", ""),
    "+34", ""
)
// "+34 612-345-678" → "612345678"

// ✅ Solo la primera instancia
SUBSTITUTE("aaa", "a", "b", 1)  -- "baa" (solo cambia la primera "a")
SUBSTITUTE("aaa", "a", "b")     -- "bbb" (cambia todas)
REPLACE
Intermedio
Reemplaza N caracteres a partir de una posición específica. A diferencia de SUBSTITUTE que busca por contenido, REPLACE trabaja por posición.
// ✅ Enmascarar el número de tarjeta (mostrar solo los últimos 4 dígitos)
Tarjeta Enmascarada =
REPLACE('Clientes'[NumTarjeta], 1, LEN('Clientes'[NumTarjeta]) - 4, "****-****-****-")
// "1234567890123456" → "****-****-****-3456"

// ✅ Cambiar el año en un código de fecha
REPLACE("2023-Q1", 1, 4, "2024")  -- "2024-Q1"
REPT
Principiante
Repite un texto N veces. Muy creativo para crear barras visuales de texto, indicadores de rating y separadores.
// ✅ Barra de progreso con texto
Barra Progreso =
VAR Porcentaje = [% Objetivo]
VAR Llenos = ROUND(Porcentaje * 10, 0)    -- de 0 a 10 bloques
VAR Vacios = 10 - Llenos
RETURN
    REPT("█", Llenos) & REPT("░", Vacios) & " " & FORMAT(Porcentaje, "0%")
// Al 73%: "███████░░░ 73%"

// ✅ Rating de estrellas
Estrellas =
    REPT("★", 'Productos'[Valoracion]) &
    REPT("☆", 5 - 'Productos'[Valoracion])
// Valoracion=3 → "★★★☆☆"
🏗️

Construir cadenas de texto

CONCATENATE / Operador &
Principiante
Une dos textos. El operador & es más flexible (puede encadenar múltiples textos) y más legible. CONCATENATE solo acepta exactamente 2 argumentos.
// ❌ CONCATENATE con más de 2 textos (hay que anidar)
CONCATENATE(CONCATENATE("A", "B"), "C")

// ✅ Con & (mucho más limpio)
"A" & "B" & "C"

// ✅ Nombre completo
Nombre Completo = 'Clientes'[Nombre] & " " & 'Clientes'[Apellidos]

// ✅ Etiqueta para visual
Etiqueta = 'Productos'[Nombre] & " (" & FORMAT([Ventas], "#,##0 €") & ")"
// "Portátil Pro (12.450 €)"

// ⚠️ Números en texto — SIEMPRE usa FORMAT o TEXT
Precio Label = "Precio: " & FORMAT('Productos'[Precio], "#,##0.00 €")
// No: "Precio: " & 'Productos'[Precio]  ← puede dar resultado inesperado
CONCATENATEX
Intermedio
Itera una tabla, evalúa una expresión para cada fila, y une todos los resultados con un delimitador. Como STRING_AGG o GROUP_CONCAT en SQL. Esencial para crear listas de valores agrupados.
// ✅ Lista de productos comprados por un cliente
Productos Cliente =
CONCATENATEX(
    RELATEDTABLE('DetallesPedido'),
    'DetallesPedido'[NombreProducto],
    ", ",
    'DetallesPedido'[NombreProducto], ASC
)
// "Laptop Pro, Monitor 4K, Teclado Mecánico"

// ✅ Lista de años con ventas (para tooltip)
Años con Ventas =
CONCATENATEX(
    VALUES('Calendario'[Año]),
    'Calendario'[Año],
    " | ",
    'Calendario'[Año], ASC
)
// "2021 | 2022 | 2023 | 2024"

// ✅ TOP 3 vendedores en una celda
Top 3 Vendedores =
CONCATENATEX(
    TOPN(3, VALUES('Vendedores'[Nombre]), [Total Ventas], DESC),
    'Vendedores'[Nombre] & " (" & FORMAT([Total Ventas], "#,##0 €") & ")",
    " • "
)
// "María García (85.000 €) • Juan López (72.000 €) • Ana Martín (68.000 €)"
COMBINEVALUES
Intermedio
Une múltiples valores con un delimitador, tratando BLANK como cadena vacía. Especialmente útil para crear claves compuestas en columnas calculadas y optimizar relaciones en DirectQuery.
// ✅ Clave compuesta para relacionar tablas
'Ventas'[ClaveCompuesta] =
    COMBINEVALUES("-", 'Ventas'[Año], 'Ventas'[Mes], 'Ventas'[Region])
// 2024, 3, "Norte" → "2024-3-Norte"

// ⚠️ A diferencia de &, si algún valor es BLANK, COMBINEVALUES lo trata como ""
"A" & BLANK() & "B"                        -- "AB" (BLANK desaparece en &)
COMBINEVALUES("-", "A", BLANK(), "B")      -- "A--B" (el BLANK aparece como vacío)
🎨

Dar formato y convertir tipos

FORMAT
Principiante
Convierte cualquier valor (número, fecha, booleano) a texto con el formato especificado. Esencial para mostrar valores formateados en títulos dinámicos, tooltips y etiquetas.

Formatos de número más usados

FORMAT(1234567.89, "#,##0")           -- "1.234.568"
FORMAT(1234567.89, "#,##0.00")        -- "1.234.567,89"
FORMAT(1234567.89, "#,##0.00 €")      -- "1.234.567,89 €"
FORMAT(0.1523, "0.0%")                -- "15,2%"
FORMAT(0.1523, "0.00%")               -- "15,23%"
FORMAT(1234567.89, "0.00E+00")        -- "1,23E+06" (notación científica)
FORMAT(42, "00000")                   -- "00042" (relleno con ceros)

Formatos de fecha más usados

FORMAT(TODAY(), "DD/MM/YYYY")         -- "15/02/2025"
FORMAT(TODAY(), "DD-MMM-YYYY")        -- "15-feb-2025"
FORMAT(TODAY(), "MMMM YYYY")          -- "febrero 2025"
FORMAT(TODAY(), "YYYY-MM-DD")         -- "2025-02-15" (formato ISO)
FORMAT(TODAY(), "DDDD")               -- "sábado" (nombre del día)
FORMAT(TODAY(), "MMM")                -- "feb" (mes abreviado)

Formatos condicionales

// Con punto y coma para positivo;negativo;cero
FORMAT([Variacion], "+#,##0.00;-#,##0.00;—")
// +1234.56 → "+1.234,56"
// -1234.56 → "-1.234,56"
// 0 → "—"

Ejemplo en título dinámico

Titulo Visual =
"Ventas " & FORMAT([Total Ventas], "#,##0 €") &
" — " & FORMAT(TODAY(), "MMMM YYYY") &
" | Crecimiento: " & FORMAT([Variacion YoY], "+0.0%;-0.0%;0%")
// "Ventas 1.234.567 € — febrero 2025 | Crecimiento: +12,3%"
FIXED
Principiante
Convierte un número a texto con un número fijo de decimales y, opcionalmente, sin separadores de miles. Similar a FORMAT pero más simple.
FIXED(1234.567, 2)        -- "1.234,57"  (con separadores de miles)
FIXED(1234.567, 2, TRUE)  -- "1234,57"   (sin separadores de miles)
FIXED(1234.567, 0)        -- "1.235"     (sin decimales)
VALUE
Principiante
Convierte texto que representa un número en un número real. Fundamental cuando importas datos con números guardados como texto.
// ✅ Columna con precios como texto: "1234,56"
Precio Real = VALUE(SUBSTITUTE('Productos'[PrecioTexto], ",", "."))
// Primero normalizar el separador decimal, luego convertir

// ✅ Con protección de errores
Precio Seguro = IFERROR(VALUE('Productos'[PrecioTexto]), 0)
🔣

Unicode y caracteres especiales

UNICHAR / UNICODE
Intermedio
UNICHAR: Devuelve el carácter Unicode correspondiente a un código numérico.
UNICODE: Devuelve el código Unicode del primer carácter de un texto.
// ✅ Flechas para indicadores
Flecha Arriba = UNICHAR(8593)   -- ↑
Flecha Abajo = UNICHAR(8595)    -- ↓
Circulo Verde = UNICHAR(9679)   -- ●

// ✅ Indicador de KPI con UNICHAR
Icono KPI =
SWITCH(
    SIGN([Variacion]),
    1,  UNICHAR(9650) & " ",  -- ▲ subida
    -1, UNICHAR(9660) & " ",  -- ▼ bajada
    UNICHAR(9644) & " "       -- ▬ sin cambio
)
// Resultado: "▲ 12,3%" o "▼ 5,1%"

// ✅ Salto de línea en texto (CHAR 10)
Dos Lineas = "Primera línea" & UNICHAR(10) & "Segunda línea"
📤

TOCSV y TOJSON — exportar tablas como texto

Estas dos funciones son las más recientes de la categoría de texto y abren posibilidades muy interesantes para integración con otras herramientas.

TOCSV
Avanzado
Convierte una tabla DAX a formato CSV como texto. Permite exportar datos calculados directamente desde una medida.
// ✅ Exportar el top 10 de productos como CSV (para usar en tooltip o card)
CSV Top Productos =
TOCSV(
    TOPN(10, SUMMARIZE('Ventas', 'Productos'[Nombre], "Total", [Total Ventas]),
         [Total], DESC),
    10,   -- máximo 10 filas
    ";"   -- delimitador punto y coma
)
TOJSON
Avanzado
Convierte una tabla DAX a formato JSON como texto. Muy útil para crear payloads para APIs o para integración con Power Automate.
// ✅ Exportar datos para API externa
JSON Clientes Top =
TOJSON(
    TOPN(5, 'Clientes', [Total Compras], DESC),
    5
)
🎯

Patrones de limpieza y formato

🏆 Patrón 1: Limpieza completa de texto
Texto Limpio =
UPPER(          -- normalizar a mayúsculas
    TRIM(       -- eliminar espacios extra
        SUBSTITUTE(
            SUBSTITUTE('Datos'[Texto], "  ", " "),  -- dobles espacios
            "  ", " "
        )
    )
)
🏆 Patrón 2: Extraer número de un código alfanumérico
Numero de Codigo =
VALUE(
    SUBSTITUTE(
        SUBSTITUTE(
            SUBSTITUTE('Productos'[Codigo], "PROD", ""),
            "ES", ""),
            "-", ""
    )
)
// "PROD-ES-12345" → 12345
🏆 Patrón 3: Validar formato de email básico
Es Email Valido =
AND(
    CONTAINSSTRING('Contactos'[Email], "@"),
    CONTAINSSTRING('Contactos'[Email], "."),
    LEN('Contactos'[Email]) > 5
)
🏆 Patrón 4: Título dinámico con todo
Titulo Completo =
VAR Periodo = SELECTEDVALUE('Calendario'[Año], "Todos los años")
VAR Pais = SELECTEDVALUE('Clientes'[País], "Todos los países")
VAR Ventas = FORMAT([Total Ventas], "#,##0 €")
VAR Var = FORMAT([Variacion YoY], "+0.0%;-0.0%;—")
RETURN
    "📊 Ventas " & Pais & " — " & Periodo & " | " & Ventas & " (" & Var & ")"
🚀 Siguiente paso
¡El texto ya no tiene secretos para ti! La siguiente parada son las Funciones de Manipulación de Tablas — una de las categorías más potentes de DAX avanzado para construir tablas calculadas, hacer joins y crear estructuras de datos complejas. 🚀