jueves, 30 de diciembre de 2021

Office Scripts: Bucles WHILE y DO ... WHILE

Vamos con la segunda entrada de sentencias de control fundamentales para trabajar con Office Scripts, y hablamos de los bucles WHILE y DO ... WHILE.
El primer bucle WHILE ejecuta un bloque de código mientras una condición determinada sea verdadera, i.e., se detendrá cuando la condición sea falsa.
Su sintáxis es sencilla:
while (Condición_Prueba Lógica)
{
// nuestro código a procesar
}


El segundo bucle DO ... WHILE ejecutará un bloque de código una vez, y luego repetirá el ciclo mientras la condición se mantenga verdadera.:
do
{
// nuestro código a procesar
} while (Condición_Prueba Lógica);


Y podrías decirme... no veo la diferencia.
Bueno, la diferencia entre uno y otro es que en el bucle DO...WHILE, el bloque a procesar siempre se ejecuta, al menos, una vez. Sin embargo con WHILE podría ocurrir que si el primer paso del bucle nis siquiera cumpliera la condición dada, NO se ejecutara nada...
Interesante...

Veamos un par de ejemplos...
En el primer ejemplo recorremos valores impares mientras que el valor devuelto sea inferior a 10:
function main(workbook: ExcelScript.Workbook) {
  // mostramos impares de 1 al 10
  // definimos variables
  let i = 1, limite = 10;

  // bucle while de 1 a 10
  while (i <= limite) 
  {
    console.log(i);
    i +=2;
  }
}

El bucle empieza en 1, y va incrementando el valor de 2 en 2, mientras/while el valor devuelto sea inferior a 10.
Office Scripts: Bucles WHILE y DO ... WHILE


Un segundo ejemplo con el bucle DO ... WHILE
function main(workbook: ExcelScript.Workbook)
{
  let i=1, limite=10;
  do 
  {
    console.log(i);
    i ++
  } while (i<=limite);
}

El bucle empieza en 1, y va incrementando el valor de 1 en 1, mientras el valor devuelto sea inferior a 10.

Veamos un ejemplo donde lanzamos un primer bucle WHILE y a continuación uno DO..WHILE similar (con igual condición) pero con resultados algo distintos...
 function main(workbook: ExcelScript.Workbook)
{
// diferencias entre WHILE y DO .. WHILE
let i=1, limite=4;
//1ro WHILE
while (i>2 && i<=limite)
  {
  console.log("bucle while " + i);
    i ++
  }

let j = 1, limite2 = 4;
//2do DO...WHILE
do 
{
  console.log("bucle do...while " + j);
  j++
} while (j > 2 && j<=limite2);
}

Al lanzar este Script vemos como solo se ejecuta el primer intento del bucle DO..WHILE!!.
El motivo, lo ya comentado.
WHILE escapa del bucle en el momento que no se cumple la condición; por tanto, en el ejemplo previo, al asignar i=1 como primer intento y NO cumplir con la condición dada (valor entre 2 y 4) sale del bucle...
Sin emabrgo, DO...WHILE si ejecuta el primer bloque... y solo después evalúa la condición, y al no cumplirla escapa del bucle.

En realidad este comportamiento es el habitual en todos los lenguajes de programación ;-)

Veamos un último ejemplo donde obtendremos un acumulado de celdas hasta cumplir que sobrepasamos el total de 250:
function main(workbook: ExcelScript.Workbook)
{
  let Acum =0;
  let fila =0; 
  let Hoja = workbook.getActiveWorksheet();
  let numero = Hoja.getCell(0,fila).getValues();
  
  do
  {
    Acum += +numero;
    fila ++;
    numero= (Hoja.getCell(fila,0).getValues());
  } while (Acum <= 250 && fila<=9) 

  // mostramos el acumulado
  console.log(`El acumulado es ${Acum}.`);
}

Office Scripts: Bucles WHILE y DO ... WHILE

Comprobamos como con nuestro bucle vamos recorriendo celdas de nuestra hoja, y acumulando valores, mientras que el total sea inferior a 250.

martes, 28 de diciembre de 2021

Office Scripts: Los bucles FOR

Me he animado a escribir una serie de artículos explicando, con ejemplos, algunas de las sentencias de control básicas para trabajar con Office Scripts (TypeScript).
Hoy comenzaré con algunos de los tipos de bucles existentes.

El primero por el que comenzaría sería por un FOR-LOOP básico, con una estructura o sintaxis:
for (variable=inicio; variable<=final; variable=variable+incremento)
{
// nuestro código a procesar
}


Un ejemplo de este tipo de bucle podría ser el siguiente:
function main(workbook: ExcelScript.Workbook)
{
  //identificamos la hoja activa
  let Hoja = workbook.getActiveWorksheet();
  
  //marcamos un bucle desde cero hasta 10
  //Recuerda que estamos en BASE CERO !!
  for (let fila = 0; fila <= 9; fila++)
  {
    //para cada valor de 0 a 9 creamos un número entero aleatorio
    let aleatorio = Math.floor(Math.random() * (100 - 0 + 1) + 0);
    //y lo trasladamos a las celdas de la columna A
    Hoja.getCell(fila, 0).setValue(aleatorio);
  }
}

Office Scripts: Los bucles

Este ejemplo recorre los números de 0 hasta 9, que luego empleamos para identificarlos como filas de la columna A, y añadir a cada celda un número entero aleatorio previamente generado.


Otro bucle sería el FOR-IN con sintaxis:
for (variable in object)
{
// nuestro código a procesar
}


Por ejemplo:
function main(workbook: ExcelScript.Workbook)
{
  //identificamos la hoja activa
  let Hoja = workbook.getActiveWorksheet();
  //declaramos el objeto del rango A1:A10
  let enteros=Hoja.getRange("A1:A10").getValues()

  //y lo recorremos.. llevando los valores a la Consola
  for (let i in enteros) 
  {
    console.log(enteros[i])
  }
}

Office Scripts: Los bucles

En el ejemplo recorremos el rango de celdas A1:A10 y la mostramos en la 'Consola' (algo parecido a la ventana de inmediato en VBA para Excel.

Otra forma sería el FOR_OF:
for (variable of object)
{
// nuestro código a procesar
}


Veamos un ejemplo:
function main(workbook: ExcelScript.Workbook) {
  //identificamos la hoja activa
  let Hoja = workbook.getActiveWorksheet();
  //declaramos el objeto del rango A1:A10
  let enteros = Hoja.getRange("A1:A10").getValues()

  //y lo recorremos.. llevando los valores a la Consola
  for (let i of enteros) 
  {
    //OJO con la forma de referirnos a los elementos del objeto!!
    console.log(i)
  }
}

Relevante la manera en que nos referenciamos a los elementos del objeto indicado, distinto al anterior FOR-IN.

Un último bucle FOR, sería aplicable al trabajar con Arrys/Matrices, hablo de FOREACH, con sintáxis:
matriz.forEach(function(variable, i)
{
// nuestro código a procesar
})


Por ejemplo:
function main(workbook: ExcelScript.Workbook) 
{
  //identificamos la hoja activa
  let Hoja = workbook.getActiveWorksheet();
  //declaramos el objeto del rango A1:A10
  let enteros = Hoja.getRange("A1:A10").getValues();
  
  //y lo recorremos.. llevando los valores a la Consola
  enteros.forEach( Num =>
  {
    console.log(Num);
  });
}


En siguientes artículos continuaré con estas y otras interesantes sentencias de control...

jueves, 23 de diciembre de 2021

Grafico con controles de formulario

Hace bastante que no publico sobre gráficos.. asi que hoy me he animado, tras una formación donde necesitaban aplicar algo especial sobre un gráfico.
Se trata de incorporar unos filtros temporales, donde indicar el inicio y fin de un intervalo de meses, a un gráfico. Lo que conseguiremos añadiendo dos controles de formulario tipo barras de desplazamiento.
Grafico con controles de formulario


Necesitamos añadir dos Barras de desplazamiento en las celdas D1:E1 y D2:E2, la primera con el vínculo a la celda E1, y la segunda a E2, y ambas con el rango entre 1 y 12:
Grafico con controles de formulario
Estos controles de formulario los puedes encontrar en la ficha Programados > grupo controles > desplegable Insertar > Controles de formulario (NO Active X) > Barra de desplazamiento
Grafico con controles de formulario


El resto es algo de formulación...
Por estética añadimos en F1 la fórmula:
=INDICE($C$4:$N$4;1;$E$1)
para mostrar el mes de inicio del detalle a mostrar.
Y añadimos en F2 la fórmula:
=INDICE($C$4:$N$4;1;$E$2)
para mostrar el mes de finalización del detalle a mostrar.
En ambos casos recuperamos el mes a partir del encabezado de los datos completos del rango B4:N7.

En el siguiente paso crearemos los siguientes nombres definidos (que luego incorporaremos al gráfico):
chtDatosTraspuestos =TRANSPONER(DESREF(Hoja1!$B$4;1;Hoja1!$E$1;3;(Hoja1!$E$2-Hoja1!$E$1+1)))
chtEtiquetas =TEXTO(TRANSPONER(DESREF(Hoja1!$B$4;0;Hoja1!$E$1;1;Hoja1!$E$2-Hoja1!$E$1+1));"mmm-aa")
chtSeries =TRANSPONER(Hoja1!$B$5:$B$7)
Serie_A =INDICE(chtDatosTraspuestos;0;1)
Serie_B =INDICE(chtDatosTraspuestos;0;2)
Serie_C =INDICE(chtDatosTraspuestos;0;3)

Donde recuperamos rangos variables, asociadas a las celdas del rango con datos B4:N7, empleando la elección realizada con nuestras barras de desplazamiento.
Tendríamos un nombre definido para las etiquetas del eje, esto es los meses:
chtEtiquetas =TEXTO(TRANSPONER(DESREF(Hoja1!$B$4;0;Hoja1!$E$1;1;Hoja1!$E$2-Hoja1!$E$1+1));"mmm-aa")
Además lo devolvemos en forma de texto pero con formato 'mmm-aa'
Igualmente para los 'Conceptos' con el nombre definido:
chtSeries =TRANSPONER(Hoja1!$B$5:$B$7)
Y además obtenemos el conjunto de datos/valores de las series:
chtDatosTraspuestos =TRANSPONER(DESREF(Hoja1!$B$4;1;Hoja1!$E$1;3;(Hoja1!$E$2-Hoja1!$E$1+1)))
A partir de ese rango variable de importes generamos las tres series a mostrar, que obtenemos con la función INDICE:
Serie_A =INDICE(chtDatosTraspuestos;0;1)
Serie_B =INDICE(chtDatosTraspuestos;0;2)
Serie_C =INDICE(chtDatosTraspuestos;0;3)

Listos... creamos un gráfico 'normal' de columnas agrupadas sobre los datos originales que muestra la totalidad de los meses:
Grafico con controles de formulario
Nos servirá como punto de partida para nuestra personalización...
En mi ejemplo lo mantendré como comparativa y trabajaré sobre una copia de este...

Accedemos a la opción de Seleccionar datos del gráfico a personalizar
Grafico con controles de formulario

Accederemos a Editar las etiquetas del eje horizontal, donde cambiaremos los rango estáticos por nuestros rangos variables (creados con los nombres definidos):
Grafico con controles de formulario

Notemos que el rango se escribe incluyendo el nombre de la hoja (o el del libro) delante:
=CHRT_conControlesForm.xlsx!chtEtiquetas

Repetiremos los pasos editando las tres series:
Grafico con controles de formulario

El rango se escribe incluyendo el nombre de la hoja (o el del libro) delante:
=CHRT_conControlesForm.xlsx!Serie_A

No olvides repetir el paso para las otras series de datos!!

LISTO... ya puedes jugar con las barras de desplazamiento para mostrar más o menos meses en el gráfico!!

martes, 21 de diciembre de 2021

VBA: Imagen asociada a valor celda

Creo que el ejercicio de hoy es algo muy útil y prácatico. Se trata de crear, con VBA para Excel, una UDF que permita asociar una imagen con el dato de una celda.
Claro, podrás decir que eso ya es posible con fórmulas (DESREF, INDICE, INDIRECTO...), de hecho en este mismo blog ya he publicado al respecto:
Un clásico siempre funcional con DESREF
Añadiendo objetos
Con INDIRECTO
y algún caso más, empleando INDICE y COINCIDIR (o incluso BUSCARV)...

Pero todos estos ejemplos tienen un problema NO ADMITEN referencias relativas!!, es decir, si queremos aplicar el método a diez celdas, habría que gestionar diez modelos distintos, i.e., diez nombres definidos distintos, uno por cada celda destino.... y si en lugar de diez son cien??
:OO
Nada práctico

De aquí nace la necesidad de esta Función personalizada, que permitirá aplicar rápidamente en cualquier circunstancia, y con un número ínfimo de nombres definidos esta funcionalidad...

Nuestro punto de partida será nuestro Banco de fotos en una hoja de nuestro libro, donde se listan diferentes códigos y en la celda de su derecha una imágenes situadas y centradas en esas celdas...
Celdas a las que hemos asignado nombres definidos:
A01_ =Hoja1!$C$3
A02_ =Hoja1!$C$4
A03_ =Hoja1!$C$5
A04_ =Hoja1!$C$6

VBA: Imagen asociada a valor celda

La idea es dinamizar la busqueda en los rangos de la derecha (F3:G6 y F9:G13) para que según cambiemos el valor en F3:F6 y en F9:F13, la imagen de su derecha, en la columna G, se ajuste según corresponda...
Por supuesto, NO OLVIDES que las imagenes sobre las que trabajamos responde a imágenes vinculadas!!
Para ello necesitamos algo más... en una celda cualquiera insertamos una autoforma a la que hemos cambiado el nombre por 'Comodin'.
Esta parte es importante ya que será la que evitará que, tras la ejecución de nuestra función, las imágenes queden seleccionadas (TRUCO!!!)
En mi caso lo he añadido encima de la celda F1 (pero puede ser cualquiera!!). Nuestra macro se encargará de ocultarla y/o hacerla visible segú corresponda...

Vamos entonces con el código VBA de nuestra UDF.
He creado dos UDF, una para centrar la imagen en la celda (capricho estético no necesario):
Function CentrarImagenCelda(ByVal imagen As Shape, ByVal celda As Range)
        
    Set miImagen = imagen
    Set celdaImagen = celda
    'centramos la imagen
    miImagen.Top = celdaImagen.Top + (celdaImagen.Height / 2) - (miImagen.Height / 2)
    miImagen.Left = celdaImagen.Left + (celdaImagen.Width / 2) - (miImagen.Width / 2)
    
    'liberamos memoria
    Set miImagen = Nothing
    Set celdaImagen = Nothing

End Function

Esta función trabaja sobre el tamaño de la celda donde se encuentra la imagen, identificando su altura y anchura, así como su punto de inicio (.Top y .Left), para aplicárselo a la imagen seleccionada...

Por otra parte el código de la función que realiza el cambio de la imagen:
Function CambioFormulaImagen(vinculo As Range, FormaRef As String, celdaImagen As Variant)
'ARGUMENTOS:
'1ro 'vinculo' = será el nombre definido asociado a la imagen fuente a recuperar
'2do 'FormaRef' = Celda o Nombre, para identificar la celda o el nombre de la imagen
'3ro 'celdaImagen' = celda donde se ubica la imagen a modificar su fórmula

Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual

If UCase(FormaRef) = "CELDA" Then
    GoTo celda
ElseIf UCase(FormaRef) = "NOMBRE" Then
    GoTo nombre
Else
    GoTo celda
End If

''''''''''
'controlamos qué tipo de referencia se ha indicado
celda:
For Each pic In ActiveSheet.Shapes
    'https://docs.microsoft.com/es-es/office/vba/api/office.msoshapetype
    If pic.Type = msoPicture And celdaImagen.Address = pic.TopLeftCell.Address Then
        Set img = pic
        Exit For
    End If
Next pic
'cambiamos el vínculo/fórmula de la imagen
img.Select
Selection.Formula = vinculo.Value & "_"

'PLUS!! centramos la imagen en la celda
Call CentrarImagenCelda(img, celdaImagen)

GoTo siguiente

'''''''''''''''
'controlamos qué tipo de referencia se ha indicado
nombre:
ActiveSheet.Shapes(celdaImagen).Select
Selection.Formula = vinculo.Value & "_"

'''''''''''''''
siguiente:

'retornamos un ok si todo ha ido bien ;-)
CambioFormulaImagen = "ok"

'para dejar seleccionada una celda...
'truco para 'engañar' a Excel
With ActiveSheet.Shapes("Comodin")
    .Visible = True
    .Select
    .Visible = False
End With

vinculo.Select

'liberamos memoría!!
Set img = Nothing

Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic

End Function


Esta función de VBA tiene varias partes...
En la primera gestionamos la variabilidad ofrecida al usuario para trabajar de acuerdo al nombre concreto de la imagen o bien trabajar con la imagen posicionada en una celda concreta indicada (independientemente de cuál sea su nombre!!):
Function CambioFormulaImagen(vinculo As Range, FormaRef As String, celdaImagen As Variant)
'ARGUMENTOS:
'1ro 'vinculo' = será el nombre definido asociado a la imagen fuente a recuperar
'2do 'FormaRef' = Celda o Nombre, para identificar la celda o el nombre de la imagen
'3ro 'celdaImagen' = celda donde se ubica la imagen a modificar su fórmula

Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual

If UCase(FormaRef) = "CELDA" Then
    GoTo celda
ElseIf UCase(FormaRef) = "NOMBRE" Then
    GoTo nombre
Else
    GoTo celda
End If

[...]

End Function

La siguiente parte del código, según la elección anterior, localiza la imagen entre todas las existentes (por tipo y dirección de celda), o bien ataca directamente a la imagen en caso de conocer su nombre..
Function CambioFormulaImagen(vinculo As Range, FormaRef As String, celdaImagen As Variant)
[...]
'controlamos qué tipo de referencia se ha indicado
celda:
For Each pic In ActiveSheet.Shapes
    'https://docs.microsoft.com/es-es/office/vba/api/office.msoshapetype
    If pic.Type = msoPicture And celdaImagen.Address = pic.TopLeftCell.Address Then
        Set img = pic
        Exit For
    End If
Next pic
'cambiamos el vínculo/fórmula de la imagen
img.Select
Selection.Formula = vinculo.Value & "_"

'PLUS!! centramos la imagen en la celda
Call CentrarImagenCelda(img, celdaImagen)

GoTo siguiente

'''''''''''''''
'controlamos qué tipo de referencia se ha indicado
nombre:
ActiveSheet.Shapes(celdaImagen).Select
Selection.Formula = vinculo.Value & "_"

'''''''''''''''
siguiente:

'retornamos un ok si todo ha ido bien ;-)
CambioFormulaImagen = "ok"

[...]

End Function

Si te fijas bien, no he sido capaz de documentar otra forma, debemos seleccionar la imagen para trabajar sobre ella, y poder cambiar la fórmula asociada, que en definitiva será lo que cambie el aspecto de nuestra imagen.

La parte final es el truco que evita que la imagen anterior quede seleccionada :O
Function CambioFormulaImagen(vinculo As Range, FormaRef As String, celdaImagen As Variant)
[...]

'para dejar seleccionada una celda...
'truco para 'engañar' a Excel
With ActiveSheet.Shapes("Comodin")
    .Visible = True
    .Select
    .Visible = False
End With

vinculo.Select

'liberamos memoría!!
Set img = Nothing

Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic

End Function

Donde como indicábamos anteriormente el truco es, sobre aquella autoforma insertada (y renombrada como 'Comodin'), aplicarles propiedades para hacerla visible, seleccionarla y volver a hacerla invisible.
Con esta secuencia conseguimos 'deseleccionar' cualquier cosa ;-)

Y nuestra función está lista para trabajar en dos modos.
1- modo nombre de la imagen:
VBA: Imagen asociada a valor celda

En este caso se necesita que todas las imágenes vinculadas tengan nombres distintos!!, lo que hace el trabajo más laborioso. Pero debemos cubrir distintas posibilidades ;-)

2- modo posición de celda:
VBA: Imagen asociada a valor celda

Lo interesante de este caso es que no importa el nombre de la imagen, bastando indicar la celda donde se ubique la imagen 👏👏 No se si habrá un método alternativo o más simple... pero después de rebuscar entre diferente documentación, esta ha sido mi mejor aproximación ;-)

jueves, 16 de diciembre de 2021

DAX: Media Ponderada en Tabla dinámica

Tiempo atrás escribí un artículo donde explicaba un uso interesante de la función DAX en Power Pivot HASONEVALUE en el modelo de datos (leer más aquí).
Esta función, entre otras cosas, nos permite diferenciar cálculos para las filas y para los Subtotales/Totales generales de una tabla dinámica.

Hoy daremos una vuelta al tema tratado, del cálculo de las medias ponderadas en tablas dinámicas, ya que buscaremos una medida en el modelo de datos de Power Pivot que muestre una media ponderada sobre un total anual, mientras que los subtotales/totales generales lo mostrarán sobre los datos solo de un trimestre.
DAX: Media Ponderada en Tabla dinámica


Si revisas la tabla dinámica con detalle, cargada a partir del modelo de datos, comprobaras en las columnas I, J y K el cálculo mostrado a partir de las siguientes medidas:
    PMP_TA:=SUMX(Tabla1;Tabla1[Ventas_EUR])/SUMX(Tabla1;Tabla1[Ventas Anuales])
PMP_Q4:=SUMX(Tabla1;Tabla1[Q4]*Tabla1[Precio_Unitario])/SUMX(Tabla1;Tabla1[Q4])
Media_Ponderada:=IF(HASONEVALUE(Tabla1[Categoría]);[PMP_Q4];[PMP_TA])
Medidas que replican el comportamiento de un 'campo calculado' estándar de una tabla dinámica, esto es, primero suman o acumulan los campos involucrados, para luego realizar la operación definida...
Nos muestra el cálculo del precio medio ponderado para cada 'Categoría' bien sobre el Total de las unidades vendidas en el año, o bien sobre las unidades vendidas en el cuarto trimestre.
Como cualquier campo calculado estándar en una tabla dinámica, estas medidas se aplican igualmente, i.e., con el mismo cálculo, en las filas de Totales generales (o Subtotales).

En nuestro caso queremos que a nivel de fila muestre la media ponderada calculada sobre el cuarto trimeste, mientras que (por distintos motivos) en la fila de 'Total general' muestre la media ponderada calculada sobre el Total del año... y es aquí donde HASONEVALUE entra a jugar..., ya que en la medida creada:
Media_Ponderada:=IF(HASONEVALUE(Tabla1[Categoría]);[PMP_Q4];[PMP_TA])
con un condicional IF derivamos hacía un cálculo u otro según el caso!!!.

Como apunte final, vemos en la medida del Precio Medio Ponderado del Cuarto Trimestre:
PMP_Q4:=SUMX(Tabla1;Tabla1[Q4]*Tabla1[Precio_Unitario])/SUMX(Tabla1;Tabla1[Q4])
cómo hemos cálculo el importe en euros del Q4, de manera interna, sin necesidad de agregar un campo formulado en la tabla origen...

Sin duda trabajar con tablas dinámicas alimentadas por el Modelo de datos es siempre buena idea.

martes, 14 de diciembre de 2021

Dígitos, minúsculas, mayúsculas y otros caracteres

Una vuelta de tuerca al asunto de recuperar únicamente los dígitos, o las mayúsculas, o las minúsculas... o a caso cualquier otro caracter distintos de los anteriores.
Al hilo de las publicaciones pasadas:
Recuperar caracteres no numéricos
Recuperar caracteres numéricos
Mi colega Rick Rothstein (también MVP Excel) me recordó algunas alternativas para obtener de otra forma caracteres de texto o dígitos... lo que me llevó a este artículo.
Dígitos, mayúsculas, minúsculas y otros caracteres

Veamos las ideas propuestas por Rick para los tres primeros casos.

Para recuperar los dígitos de la cadena... en C2:C7 insertamos:
=LET(x;EXTRAE(B2;SECUENCIA(LARGO(B2));1);
          CONCAT(SI(ESNUMERO(ENCONTRAR(x;"0123456789"));x;"")))

Dígitos, mayúsculas, minúsculas y otros caracteres

Lo interesante de la propuesta es el uso de ENCONTRAR (recuerda que esta función discrimina mayúsculas de minúsculas) para localizar cada caracter en la secuencia indicada "0123456789".
El resto de la técnica es similar a lo expuesto en los artículos del blog indicados al inicio...

Para recuperar las mayúsculas de la cadena... en D2:D7 insertamos:
=LET(x;EXTRAE(B2;SECUENCIA(LARGO(B2));1);
      CONCAT(SI(ESNUMERO(ENCONTRAR(x;"ABCDEFGHIJKLMNÑOPQRSTUVWXYZ"));x;"")))

Dígitos, mayúsculas, minúsculas y otros caracteres
Identificamos cualquier letra del alfabeto en mayúsculas...

De igual forma para recuperar las minúsculas de la cadena... en E2:E7 insertamos:
=LET(x;EXTRAE(B2;SECUENCIA(LARGO(B2));1);
      CONCAT(SI(ESNUMERO(ENCONTRAR(x;"abcdefghijklmnñopqrstuvwxyz"));x;"")))

Dígitos, mayúsculas, minúsculas y otros caracteres

Y finalmente, siguiendo la filosofía de Rick) para recuperar cualquier otro caracter (que no sea ni letras ni dígitos) en F2:F7 añadimos:
=LET(x;EXTRAE(B2;SECUENCIA(LARGO(B2));1);
      CONCAT(SI(ESNUMERO(HALLAR(x;"0123456789abcdefghijklmnñopqrstuvwxyz"));x;"")))

Notemos como hemos empleado la función HALLAR en lugar de ENCONTRAR, para identificar mayúsculas y minúsculas indistintamente...
Dígitos, mayúsculas, minúsculas y otros caracteres


Lo interesante de esta metodología es lo sencillo que sería personalizar una secuencia de caracteres buscados ;-)

Una alternativa para recuperar la lista de otros caracteres, para otro tipo de mentes mas 'retorcidas', podría ser en G2:G7:
=LET(vPos;SECUENCIA(LARGO(B2));
xValor;EXTRAE(B2;vPos;1);
arrNoDigits;N(SI(ESNUMERO(ENCONTRAR(xValor;"0123456789"));xValor;vPos));
arrNoMayusc;N(SI(ESNUMERO(ENCONTRAR(xValor;"ABCDEFGHIJKLMNÑOPQRSTUVWXYZ"));xValor;vPos));
arrNoMinusc;N(SI(ESNUMERO(ENCONTRAR(xValor;"abcdefghijklmnñopqrstuvwxyz"));xValor;vPos));
arrOtros;SI(arrNoDigits*arrNoMayusc*arrNoMinusc;vPos;"");
CONCAT(SI.ERROR(EXTRAE(B2;arrOtros;1);"")))

Donde trabajamos por exclusión de caracteres, recuperando la posición en la cadena inicial, de todo aquello que no sea ni dígitos, ni mayúsculas, ni minúsculas!!

jueves, 9 de diciembre de 2021

Convertir Vector en Matriz

Revisaremos en el artículo de hoy algunas alternativas para convertir, mediante fórmulas, un vector de celdas en una matriz de n filas x m columnas.
Convertir Vector en Matriz

Veremos tres métodos para la conversión buscada, i.e., para llegar del rango/vector B2:B17 a la matriz de 8 filas x 2 cols (de los rango en amarillo, en la imagen anterior).

El primer método sería el más general y quizá novedoso (no lo he visto nunca, y di con él probando otro desarrollo), ya que aunque estoy empleando la funcion LET, se podría reproducir con el empleo de nombres definidos, o incluso en una fórmula tradicional...
En D2 escribiríamos:

  =LET(
       arrPosicion;--LET(vector;$B$2:$B$17;
       vFilas;FILA(INDIRECTO("1:8"));
       vCols;TRANSPONER(FILA(INDIRECTO("1:2")));
       (vFilas&vCols));
vPosicion;K.ESIMO.MENOR(arrPosicion;FILA(INDIRECTO("1:16")));
rankPosicion;COINCIDIR(arrPosicion;vPosicion;0);
INDICE($B$2:$B$17;rankPosicion))

Convertir Vector en Matriz

Lo interesante del proceso de cálculo son las etapas iniciales, donde se compone una matriz de 8 filas x 2 cols basados en un concatenado de posiciones de filas y columnas, como se vé en la imagen siguiente:
arrPosicion;--LET(vector;$B$2:$B$17;
       vFilas;FILA(INDIRECTO("1:8"));
       vCols;TRANSPONER(FILA(INDIRECTO("1:2")));
       (vFilas&vCols))

Convertir Vector en Matriz

Esta disposición de valores nos permite en el paso siguiente obtener un vector ordenado de posiciones, ordenados de menos a más y siguiendo el orden de filas, es decir, un vector columna: 11,12,21,2,31,32, ... , 81,82:
vPosicion;K.ESIMO.MENOR(arrPosicion;FILA(INDIRECTO("1:16")))


En un paso siguiente, reconvertimos en una serie del 1 al 16 cada valor del vector anterior... es decir, pasamos de la matriz arrPosicion
11 12
21 22
31 32
41 42
51 52
61 62
71 72
81 82
a la matriz:
1 2
3 4
5 6
7 8
9 10
11 12
13 14
15 16
Justo lo que necesitamos para añadir al argumento de la función INDICE:
INDICE($B$2:$B$17;rankPosicion)

Con lo que llegamos al resultado deseado!! :O

El segundo método es mucho más simple, ya que se basa en el uso combinado de INDICE y SECUENCIA.
Siendo SECUENCIA la encargada de construir la matriz necesaria de posiciones. En G2 escribimos:
=INDICE($B$2:$B$17;SECUENCIA(8;2))

Convertir Vector en Matriz

Mucha más simple y fácil de escribir y analizar...pero requiere de la función SECUENCIA actualmente solo disponible en Microsoft 365 :'(

Un tercer método sería empleando la función DESREF, anidando en ella las clásicas funciones FILA, COLUMNA; y necesita que seamos nosotros quienes nos encarguemos de copiar y pegar la fórmula (o arrastrar) sobre el rango final esperado.
En J2 podríamos escribir:
=DESREF($B$2;2*FILA($B2)+COLUMNA(B1)-(2*FILA($B$2))-2;0)

Convertir Vector en Matriz

No olvides arrastrar desde J2 hasta K9!!!.
En este ejemplo, con el uso de la fórmula:
2*FILA($B2)+COLUMNA(B1)-(2*FILA($B$2))-2
conseguiríamos una matriz de posiciones, desde la celda B2, para obtener en el orden adecuado, cada valor en su correcta posición 'matricial':
0 1
2 3
4 5
6 7
8 9
10 11
12 13
14 15
esto es, desde B2 nos desplazamos cero filas, cero columnas... por tanto recupero, el dato de B2. Que ubico en la posición matricial fila 1 - columna 1
para el siguiente, desde B2 nos desplazamos 1 fila, cero columnas, y recupero el dato de B3. Que ubico en la posición matricial fila 1 - columna 2
para el siguiente, desde B2 nos desplazamos 2 filaa, cero columnas, y recupero el dato de B4. Que ubico en la posición matricial fila 2 - columna 1
Y sucesivamente... concluyendo con la matriz de celdas buscadas...

Seguro que habrá formas más vistosas, o incluso fáciles... pero es un buen recopilatorio de métodos para pasar de Vector a Matriz.

martes, 7 de diciembre de 2021

Fórmula para Convertir texto a matriz

Un mes atrás publiqué un ejemplo con VBA de cómo conseguir pasar cadenas de texto a matriz.
En el artículo de hoy conseguiremos lo mismo pero solo con fórmulas!!
Fórmula para Convertir texto a matriz

¿Cuántas veces hemos copiado o exportado datos de un pdf y nos ha devuelto la 'supuesta tabla' como una cadena de valores no respetando la dsitribución de columnas?... Muchas veces el problema es el cómo se generó esa tabla y se insertó en el documento y otras simplemente falla el OCR de lectura del pdf.
El caso es que lo que tenemos en nuestra hoja de cálculo es un rango de celdas, cada una con una cadena de texto con la secuencia de la columnas dispuestas seguidas... como vemos en la imagen anterior :'(
Veamos cómo con una 'formulita' conseguiremos convertir esas celdas en la matriz original esperada.

Lo primero es buscar patrones en esas celdas...
Observamos que hay en primer lugar, tanto para encabezados como para datos, una secuencia de palabras que queremos se conviertan en la primera columna.
Y el resto de datos, numéricos o vacíos, serán el resto de columnas (en mi ejemplo corresponderán a los tres años dispuestos).
Fórmula para Convertir texto a matriz

Como puedes comprobar en la imagen la fórmula no es pequeña... no puede ser de otra forma, ya que el trabajo que realiza es complejo:
1- segrega la primera parte de textos (el PAÍS) independientemente de las palabras que contenga.
Puedes verificar en la imagen que hay paises escritos de distintas formas con 1,2,3, ... palabras
2- extrae las columnas de años respetando aquellas que no tengan valor.
3- unimos ambas para componer nuestra matriz final.
Te recomiendo para los dos primero puntos revises estas dos entradas del blog:
Recuperar textos
Recuperar números
Son técnicas que emplearemos en la fórmula siguiente:
=LET(cadena;UNIRCADENAS(";";FALSO;$I$3:$I$8);
k;SECUENCIA(LARGO(cadena));

titulos;LET(tratado1;LET(at;EXTRAE(cadena;k;1);
                                                           SI(CODIGO(at)<=57;SI(CODIGO(at)>=48;"";at);at)               );
                             UNIRCADENAS("";VERDADERO;tratado1));
calculo1;XMLFILTRO("<i><d>"&SUSTITUIR(titulos;";";"</d><d>")&"</d></i>";"//d");
espaciosCab;LARGO(calculo1)-LARGO(SUSTITUIR(calculo1;" ";""));

numeros;LET(tratado2;LET(an;EXTRAE(cadena;k;1);
                                                           SI(CODIGO(an)>59;"";an)               );
                           txt; (UNIRCADENAS("";VERDADERO;tratado2));
                            txt  );
calculo2;XMLFILTRO("<i><d>"&SUSTITUIR(SUSTITUIR(numeros;" ";"\");";";"</d><d>")&"</d></i>";"//d");

cadenafinal;UNIRCADENAS(";";FALSO;calculo1&EXTRAE(calculo2;espaciosCab+1;LARGO(calculo2)));
filas; (LARGO(cadenafinal)-LARGO(SUSTITUIR(cadenafinal;";";"")))+1;
cols; ((LARGO(cadenafinal)-LARGO(SUSTITUIR(cadenafinal;"\";"")))/filas)+1;
elementos; SUSTITUIR(cadenafinal;"\";";");
separoElementos;   LET(n; cols*filas; kf; SECUENCIA(n);
                                         ini; SI.ERROR( ENCONTRAR("|";SUSTITUIR(elementos;";";"|";kf-1))  +1;1);
                                         fin; SI.ERROR( ENCONTRAR("|";SUSTITUIR(elementos;";";"|";kf));LARGO(elementos)+1);
                                          ESPACIOS(EXTRAE(elementos; ini; fin-ini))         );
 textoMatriz;   LET( matriz; SECUENCIA(filas;cols);
                                        INDICE( separoElementos; matriz)        );
textoMatriz)

Analicemos nuestra fórmula por partes...
En primer lugar identificamos el rango de celdas y lo concatenamos con la función UNIRCADENAS, para a continuación realizar un conteo de caracteres total... necesario para posteriormente proceder a una extracción caracter a caracter:
=LET(cadena;UNIRCADENAS(";";FALSO;$I$3:$I$8);
k;SECUENCIA(LARGO(cadena))


En segundo lugar recuperaremos, bajo cualquier condición (dará igual el número de palabras que tenga) la lista de paises, que será la columna uno.
titulos;LET(tratado1;LET(at;EXTRAE(cadena;k;1);
                                                           SI(CODIGO(at)<=57;SI(CODIGO(at)>=48;"";at);at)               );
                             UNIRCADENAS("";VERDADERO;tratado1));
calculo1;XMLFILTRO("<i><d>"&SUSTITUIR(titulos;";";"</d><d>")&"</d></i>";"//d");
espaciosCab;LARGO(calculo1)-LARGO(SUSTITUIR(calculo1;" ";""))

Al repasar caracter a caracter excluímos cualquier caracter numérico, esto es, nos quedamos solo con los caracteres de texto, espacios en blanco, comas, punto y coma, etc...
Que luego uniremos en una nueva cadena de texto única con UNIRCADENAS, para convertirlo finalmente con XMLFILTRO en un vector de paises...
Con lo que ya tendremos nuestra primera columna de la matriz final...

En el tercer paso obtendremos la parte numérica, esto serán las columnas de los años con los valores numéricos o vacíos.
numeros;LET(tratado2;LET(an;EXTRAE(cadena;k;1);
                                                           SI(CODIGO(an)>59;"";an)               );
                           txt; (UNIRCADENAS("";VERDADERO;tratado2));
                            txt  );
calculo2;XMLFILTRO("<i><d>"&SUSTITUIR(SUSTITUIR(numeros;" ";"\");";";"</d><d>")&"</d></i>";"//d")

Similar proceso al anterior, pero discriminando todos los caracteres de texto, pero respetando los espacios en blanco (que serán futuras columnas sin dato).
Revisa las entradas comentadas para entender mejor estos dos pasos:
Recuperar textos
Recuperar números

El siguiente paso consiste en unir los dos resultados previos (países más valores), para identificar a continuación el número de filas y columnas existentes (según los caracteres separadores incluidos):
cadenafinal;UNIRCADENAS(";";FALSO;calculo1&EXTRAE(calculo2;espaciosCab+1;LARGO(calculo2)));
filas; (LARGO(cadenafinal)-LARGO(SUSTITUIR(cadenafinal;";";"")))+1;
cols; ((LARGO(cadenafinal)-LARGO(SUSTITUIR(cadenafinal;"\";"")))/filas)+1;
elementos; SUSTITUIR(cadenafinal;"\";";");

Contamos el número de filas y columnas según el número de separadores (; para filas y \ para columnas) existentes...
Y una vez conocidos, convertimos todos los separadores en ;
El sentido de esto es disponer seguidamente de un único vector de todos los datos países, vacíos y valores, con una cadencia de distribución...

Así pues llegamos casi al final. Con la última sustitución realizada, aplicamos una alternativa a XMLFILTRO para separar elementos (siempre es bueno conocer otros métodos de trabajo):
separoElementos;   LET(n; cols*filas; kf; SECUENCIA(n);
                                         ini; SI.ERROR( ENCONTRAR("|";SUSTITUIR(elementos;";";"|";kf-1))  +1;1);
                                         fin; SI.ERROR( ENCONTRAR("|";SUSTITUIR(elementos;";";"|";kf));LARGO(elementos)+1);
                                          ESPACIOS(EXTRAE(elementos; ini; fin-ini))         )

Personalmente, y mientras Microsoft no libere alguna función tipo SPLIT, prefiero XMLFILTRO ;-)
El vector obtenido tendrá tantos elementos como el resultante de multiplicar filas por columnas, distribuidos de acuerdo a un patrón:
Datos de la primera fila completa, después datos de la segunda fila, etc...

El último paso de la fórmula se encarga de convertir un vector en matriz:
textoMatriz;   LET( matriz; SECUENCIA(filas;cols);
                                        INDICE( separoElementos; matriz)        );
textoMatriz

Para lo cual nos apoyamos en INDICE y en SECUENCIA para generar la matriz de distribución de filas por columnas.
Fórmula para Convertir texto a matriz

Algo larga, pero con un buen resultado ;-)

jueves, 2 de diciembre de 2021

Recuperar caracteres no numéricos

Al hilo de la entrada anterior del blog, donde explicaba alternativas para obtener números entre cadenas de texto...
Hoy, para responder a algún lector que preguntaba por lo contrario, esto es, por quedarnos únicamente con caractéres no numéricos de una celda, montaremos la siguiente fórmula:
=LET(cadena;B2; tratado;LET(k;SECUENCIA(LARGO(cadena)); a;EXTRAE(cadena;k;1); SI(CODIGO(a)>=48;SI(CODIGO(a)<=57;"";a);a) ); UNIRCADENAS("";VERDADERO;tratado))
Recuperar caracteres no numéricos

Recordemos que los códigos ANSI relevantes para el caso son:
0 - ANSI 48
1 - ANSI 49
2 - ANSI 50
...
9 - ANSI 57
por lo que en este caso estamos dejando fuera de la composición cualquier caracter numérico...

Otra opción, si únicamente queremos recuperar letras mayúsculas, trabajaríamos sobre los códigos ANSI 65 y 90:
=LET(cadena;B2; tratado;LET(k;SECUENCIA(LARGO(cadena)); a;EXTRAE(cadena;k;1); SI(CODIGO(a)>=65;SI(CODIGO(a)<=90;a;"");"") ); UNIRCADENAS("";VERDADERO;tratado))
donde dejaríamos atrás cualquier otro signo o caracter
Recuperar caracteres no numéricos


O finalmente si solo nos interesaran las letras minúsculas.. dejando fuera cualquier otro caracter trabajreíamos sobre los códigos ANSI 97 y 122...

Por no olvidar nuestra eñe: Ñ - ANSI 209
ñ - ANSI 241
Las letras con tilde (acentuadas) son:
Á 193
É 201
Í 205
Ó 211
Ú 218

á 225
é 233
í 237
ó 243
ú 250