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.
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.
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.
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. 🚀
¡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. 🚀