martes, 29 de mayo de 2018

Arrays - Ventajas y desventajas


Me encantan las fórmulas que utilizan los arrays, tanto arrayformula() que consigue que una fórmula se extienda a todas las celdas que queramos, como la más prosáica, pero muy potente {} que nos permite llevar, sin más, grupos de valores de un lado a otro, juntarlos, ponerlos unos al lado de otros, utilizando {a:a;c:c} (nos pone en la misma columna los contenidos de la columna A y luego los de la C) y {a:a\s:s} que pone dos columnas separadas una al lado de otra. Os la recomiendo vívamente.

Pero creo que, no estoy seguro del todo, los arrays provocan que se creen filas adicionales vacías. Os cuento mi caso:

Tengo un código que sirve para que los profesores envíen mediante formulario partes de convivencia. Al enviarse el formulario, el código manda un mensaje a la familia del alumno, otro al tutor y otro más al jefe de estudios.

El código no tiene bucles, ni grandes peticiones al servidor, por lo que se ejecuta en apenas unos segundos.

Pues, resulta, que empezó a dejar de funcionar ya que se pegaba un montón de tiempo ejecutándose y al final se cortaba.

Ayer estuve toda la mañana haciendo pruebas, corriendo el código por partes y volvía al mismo problema.

Probé a volver a crearlo en otra hoja de cálculo, poco a poco, y conseguí que funcionase sin problemas. Copié este código nuevo en la anterior hoja y, otra vez, se quedaba colgado ejecutándose, por lo que, por lógica, algo tenía la primera hoja que ralentizaba tanto la ejecución.

Se me ocurrió ir limpiando la hoja de cálculo de columnas y filas vacías y.. me encontré que en una pestaña de 800 registros había unas 40.000 filas blancas.  Borré todas estas y... el código volvió a funcionar perfectamente.
Pero, me fijé que cada vez que corría el código, volvían a aparecer filas blancas en dicha pestaña. A estas alturas podéis imaginar que las fórmulas de esta pestaña estaban construídas con arrays y que debería haberme dado cuenta que un array, por ejemplo ={a:a;c:c}, te junta dos columnas en una sola, pero todas las filas vacías de cada una se unen y se duplican.

En conclusión y como solución, podemos decir que los arrays son muy potentes, pero pueden crear muchas filas vacías que, con el tiempo, pueden influir hasta tal punto que no dejen que un código sencillo funcione.

Medidas:
  • En la fórmula con array es aconsejable meter un filtro que quite las filas vacías del resultado, algo como así:  =FILTER({A2:D;L2:O};{A2:A;L2:L}<>"")
  • Para curarnos en salud, es bueno tener una función que limpie las filas vacías y que se incluya en onOpen(), para que corra al abrir la herramienta. Yo la he definido así:
  • function limpiar_filas(){
    var aplicacion = SpreadsheetApp.getActiveSpreadsheet()
    var exportar = aplicacion.getRangeByName('exportar').getSheet()
    Logger.log(exportar.getRange('G1').getValue())
    exportar.getRange('E1').setValue('=counta(Q:Q)')
    var ultima_datos = exportar.getRange('E1').getValue()
    Logger.log(ultima_datos)
    var ultima = exportar.getLastRow()
    Logger.log(ultima)
    if (ultima > ultima_datos){ // Si no hacemos esta comparación nos ocurrirá que cuando todo esté limpio ultima y ultima_datos serán iguales y la función no correrá y dará error
    exportar.deleteRows(ultima_datos+parseInt(1), ultima-ultima_datos)
    }
    }

domingo, 27 de mayo de 2018

Trabajar con fechas




Siempre que necesito una fecha me he encontrado con que los meses aparecen en inglés y he tenido que hacer una tabla con el número de cada mes y al lado el nombre del mes en castellano. Luego con vlookup() lo conseguía traducir.

Sin embargo, el camino es mucho más corto en código si utilizamos el método "toLocaleDateString() de javascript. Con solo dos líneas conseguimos la fecha actual exactamente como queremos:
  var today= new Date()
   var hoy=today.toLocaleDateString("es-ES") 
Hoy nos aparecerá así: 27 de mayo de 2018
 Gracias Txema

Función para trabajar con el texto de las fechas
=to_text(fecha)

Si escribimos en A3 una fecha
27 de mayo de 2018
Y luego escribimos en otra celda esta cadena
="Hoy, "&(A3)&", es el día para recoger cebollas"
Nos sale así:
Hoy, 43247, es el día para recoger cebollas

Para que nos salga bien necesitamos pasar la fecha a cadena con to_text()

="Hoy, "&TO_TEXT(A3)&", es el día para recoger cebollas"

Me queda por investigar que provecho le podemos sacar a  getRange().getDisplayValue()

sábado, 26 de mayo de 2018

Abrir o cerrar formulario con código

Hay veces que queremos que una encuesta se pueda contestar en un determinado plazo de tiempo, pero no recibir respuestas ni antes ni después. Esto lo podemos configurar manualmente en cada formulario.
Pero, en un sistema de reclamaciones de exámenes, me pidieron que se abriese o cerrase el formulario varias veces, dependiendo de un calendario específico para cada una de una lista de asignaturas.

Un bonito reto para hacerlo con código.

Supuse que el mejor sitio para escribir el código era el propio formulario. Fué bastante sencillo y rápido, ya qu enseguida encontré el método "setAcceptingResponses(true)" que es el que nos permite, cambiando entre "true" y "false" aceptar o no respuestas. Peeeero, la lógica  es que corriese el código todo los días para comparar la fecha con la de abrir o cerrar según cada asignatura y, por desgracia, los activadores de un formulario se limitan a "abrir" (abrir el formulario en modo edición) o "al enviar el formulario" (momento poco adecuado para abrir o cerrar las respuestas)

Superada la fustración, pensé en hacerlo desde la hoja de cálculo de respuestas, ya que estas tienen bastantes más activadores y, sobre todo, el de "cada día" a una hora concreta.
Este es el código:

function puerta() {
  // Enlazamos a la hoja de cálculo
  var aplicacion = SpreadsheetApp.getActiveSpreadsheet()
  // Enlazamos a la pestaña donde se recogen las respuestas del formulario en cuestión
  // Esta hoja de cálculo tiene mas formularios, por lo que es imprescindible enlazar a la pestaña que está unida al formulario que queremos

  var reclamaciones = aplicacion.getRangeByName('reclamaciones').getSheet()
 
  //Desde la pestaña conseguimos la URL del formulario
  var formulario = reclamaciones.getFormUrl()
  // Y gracias a la clase FormApp lo abrimos
  var formu =FormApp.openByUrl(formulario)

// Para abrir o cerrar necesitamos la fecha de HOY para compararla con las fechas de abrir o cerrar
  var hoy = new Date();
// Pero he tenido problemas con new Date() ya que recoge también la hora y así no se puede comparar
  // Por lo que he extraído solamente la fecha

     var dd = hoy.getDate();
     var mm = hoy.getMonth()+1;
     var yyyy = hoy.getFullYear();
     hoy = dd+'/'+mm+'/'+yyyy;

// En la pestaña de configuración (donde está el intervalo con nombre "config" tenemos las fechas de apertura y cierre en 11 líneas diferentes
var configuracion = aplicacion.getRangeByName('config').getSheet()
var fechas_apertura = configuracion.getRange('b17:b27').getValues()
var fechas_cierre = configuracion.getRange('d17:d27').getValues()

// Creamos un bucle para recorrer todas las fechas de apertura
    for (fecha in fechas_apertura){
      var esta_fecha = fechas_apertura[fecha]
// Y vemos si hoy es igual al día que hay que abrir
                       if (esta_fecha==hoy){
// En caso afirmativo ponemos en true aceptar respuestas en el formulario
                       var control = formu.setAcceptingResponses(true)
                         }
     }

 
// Creamos un segundo bucle con las fechas de cierre 
    for (fecha2 in fechas_cierre){ 
      var esta_fecha2 = fechas_cierre[fecha2]
// Y vemos si hoy es igual al día que hay que cerrar
                        if (esta_fecha2==hoy){
// En caso afirmativo ponemos en false aceptar respuestas en el formulario, osea cerramos
                        var control = formu.setAcceptingResponses(false)
                          }
     }

}


Me costó encontrar como acceder desde la hoja de cálculo a la configuración del formulario. Al final descubrí que es gracias a la clase FormApp con el dato de getFormUrl() de esta forma:
  var formulario = reclamaciones.getFormUrl()
  var formu =FormApp.openByUrl(formulario)


Pero, también estaba la cuestión de conseguir elegir uno de los varios formularios que tenía la hoja de cálculo. Y aquí el truco es hacerlo desde la pestaña que recoge las respuetas de cada uno de los formularios. En este caso aplicacion.reclamaciones.getFormUrl(), siendo "reclamaciones" la pestaña en cuestión.

Para que funcione necesitamos ir a "Activadores del proyecto activo" y configurarlo con "Basado en el tiempo", "Temporizador por día", "la hora que os parezca bien"

domingo, 20 de mayo de 2018

Crear formulario de varias líneas dentro de una hoja de cálculo

 Este código nos va a servir para poder tener los datos que vienen de un formulario (grabados en una pestaña normalmente llamada 'Respuestas de formulario') en una pestaña diferente a la que los recoge  y que podamos cambiarlos, de tal forma que se cambien a la vez en la pestaña 'Respuestas de formulario'.
Google Script tiene alguna función configurada por defecto. Una de ellas es onEdit(event)
La función onEdit(event) tiene una potencia muy grande, tanta que necesita llenarla con condicionales que la frenen. Esta función se dispara en cuanto se cambia el contenido de cualquier celda de la hoja de cálculo. Por lo tanto para domarla podemos hacer esto:

// Primero pillamos la referencia de la celda que se ha editado
  var celda_cambiada= event.source.getActiveRange().getA1Notation();
// Y luego creamos un condicional como este
   if (celda_cambiada=="B4") {}

Así, solamente correrá el código que metamos dentro del condicional y en el caso que editemos B4

Imaginaros que en B4 ponemos una lista desplegable de poblaciones. Y en la pestaña de respuestas de formulario tenemos datos sobre centros públicos y una de las columnas (La B) nos da la población donde está dicho centro. Si escribimos lo siguiente:

function onEdit(event){
   var celda_cambiada= event.source.getActiveRange().getA1Notation();
   if (celda_cambiada=="B4") {
       formulario.getRange('A7').setValue('=filter('Respuestas de formulario'!a:h;datos!B:B=B4)')
                                             }
               } 

... conseguimos un filtro con los datos de la población elegida en B4. No muy sorprendente.
Pero, una vez conseguidos los datos, como estamos en la función onEdit, cualquier cambio que hagamos en estos datos podemos pillarlos y, con código, hacer que cambien también en la pestaña respuestas de formulario, con lo que tendríamos en una segunda pestaña la posibilidad de cambiar datos en todos los sitios a la vez, como si fuera un formulario interno.

Uno de los problemas de esta técnica es que nos molesta la fórmula que hemos puesto en A7. Nos vendría bien tener los datos filtrados, pero sin la fórmula.
¿Cómo? Metemos los datos dados por la fórmula en una variable, limpiamos la fórmula y pegamos los datos otra vez. El código quedaría algo así:
function onEdit(event){
var pestana = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('pestana')
   var celda_cambiada= event.source.getActiveRange().getA1Notation();
   if (celda_cambiada=="B4") {
pestana.getRange('A7:H').clearContent() // Hay que limpiar antes, ya que si no da error
       pestana.getRange('A7').setValue('=filter('Respuestas de formulario'!a:h;datos!B:B=B4)')
var valores = pestana.getRange('A7:H').getValues()
pestana.getRange('A7:H').setValues(valores)
                                              }
               }

Ahora vamos al proceso de conseguir cambiar en respuestas de formulario justo el mismo dato que cambiamos en donde hemos hecho el filtro. Necesitamos saber la fila donde está en respuestas de formulario el dato que queremos cambiar. Se me ocurre meter en respuestas de formulario una columna que gracias a la fórmula row() nos escriba el número de cada fila y que recuperemos este número dentro del filtro.
 Con el siguiente código ya tendríamos hecho todo, siempre que las columnas de una y otra pestaña (datos y respuestas de formulario) coincidan:
//Conseguimos lo que hemos cambiado
var dato_cambiado=event.source.getActiveRange().getValue()
// Pillamos la columna que se ha cambiado
 var Col_celda_cambiada=event.source.getActiveRange().getColumn()
//  Pillamos la fila donde se ha cambiado algo, que nos servirá para conseguir el valor de la fila donde está el mismo contenido en respuesta de formulario
 var Fila_celda_cambiada=event.source.getActiveRange().getRow()
 var fila_resp_formulario=pestana.getRange(Fila_celda_cambiada,8).getValue() //ya que el número de fila lo tenemos, en este caso, en la columna H ú 8

// con los estos datos ya podemos apuntar a respuestas de formulario y cambiar el contenido adecuado
var datos =SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Respuestas de formulario')
datos.getRange(fila_resp_formulario,Col_celda_cambiada).setValue(dato_cambiado)


sábado, 19 de mayo de 2018

Traducir casillas de verificación con dos bucles

Si alguna vez habéis utilizado casillas de verificación

 ☐   ⛝  en un formulario para conseguir más de una opción en una misma pregunta,

os habréis encontrado que todas las contestaciones elegidas se meten en la misma celda

separadas por coma. Esto nos obliga a utilizar algún metodo especial para trabajar con ellas.


En el siguiente código conseguimos traducir estas opciones elegidas a otro idioma.

 Para ello necesitamos tener en la hoja de cálculo dos columnas con la lista de opciones

y sus traducciones. Parece un problema menor, pero si tenemos, pongamos por caso

10 opciones, las diferentes combinaciones posibles como contestación pueden ser,

corregirme si me equivoco: 10*9*8*7*6*5*4*3*2*1 = 3628800 posibilidades.


function reemplazar() {
 // Enlazamos a la hoja de calculo y a la pestaña
  var aplicacion = SpreadsheetApp.getActiveSpreadsheet()
  var itzulpenak = aplicacion.getRangeByName('itzulpenak').getSheet() // Utilizamos un intervalo

con nombre para que no influya que alguien cambie el nombre de la hoja


  // necesitamos saber cuantas hay que traducir, conseguimos todas las filas de la pestaña
       var todas = itzulpenak.getLastRow()

       // Hacemos un bucle que empieza en la fila 5 y acaba en la última fila, para recorrer

 todas las celdas

  for (var fila= 5; fila<=todas;fila++){
            
             // Conseguimos el valor de cada celda a traducir                var celda = itzulpenak.getRange(fila, 32).getValue()
               
             // Necesitamos buscar cada opción y traducirla por su prima en euskera

(hacemos un split y con un bucle traducimos cada trozo con replace)
             // Para eso hacemos un split
   
             var cada_una = celda.split(",")
               
                   // Por cada celda recorremos el resultado del split con un bucle y así traducimos                              for (una in cada_una){
                               
                                 //  Si no hay nada salimos del bucle
  
                                    if(cada_una== '') {
                                            break;
                                        }
                              
                              
                                     var cada = cada_una[una].trim() // quitamos el espacio blanco que queda  al principio
                                    
                                    
                                 // utilizamos una formula para conseguir la traducción. En las columna E y F están las opciones y sus traducciones
                                     var formula = itzulpenak.getRange(1, 31).setValue('=vlookup("'+cada+'";E:F;2;false)')
                                     var resultado = itzulpenak.getRange(1, 31).getValue()
                                    
                                 // Reemplazamos la traduccion en la celda, en cada vuelta el trozo
                              correspondiente
 
                                    var celda =celda.replace(cada,resultado)
                                    
                                 // Escribimos el resultado en la columna 33, cada una en su fila
                                     var resultado = itzulpenak.getRange(fila, 33).setValue(celda)
                               }
        }
}

jueves, 17 de mayo de 2018

Utilizar las fórmulas dentro del código

Quizás hay otra forma de hacerlo... La que voy a mostrar es rápida y sencilla.

Imaginaros que en un formulario tenemos un lista de tutores y que el profesor elije uno. Gracias al código queremos que le llegue por correo al tutor elegido la información que manda el profesor.

En la hoja de cálculo tendremos una lista con dos columnas. En la columna A los nombres de los tutores y en la columna B los correos correspondientes.
Dentro de la hoja de cálculo tenemos la fórmula VLOOKUP para, a partir de un tutor dado, nos dé el correo correspondiente. Por ejemplo: =VLOOKUP("Mateo Rodriguez";A:B;2;false). Así la fórmula busca a Mateo en la toda la columna A y nos devuelve lo que hay en la columna 2 (la B) a la derecha de Mateo (osea su correo).

Pero en código, que yo sepa no se pueden utilizar las fórmulas y, hasta ahora, para conseguir lo que hace la fórmula hemos utilizado un bucle y un condicional interno.

La solución que propongo son simplemente dos líneas de código:
var tutor ="Mateo Rodriguez"
var busco_correo = SpreadsheetApp.getRangeByName('datos').getRange('C1').setValue('=VLOOKUP("Mateo Rodriguez";A:B;2;false)')

var correo =SpreadsheetApp.getRangeByName('datos').getRange('C1').getValue()

En la primera variable escribo la fórmula en C1 por ejemplo.
En la segunda consiguo justamente el valor que hay en C1,  que la fórmula acaba de calcular.

Realmente la primera variable (busco_correo) quedaría así:
 var tutor = e.Values[1]
var busco_correo = SpreadsheetApp.getRangeByName('datos').getRange('C1').setValue('=VLOOKUP("'+tutor+'";A:B;2;false)')

Hoja de cálculo - Dos formularios (Código para cada uno)

Cuando solamente tenemos un formulario en la hoja de cálculo, la forma de gestionar las respuestas al formulario desde código es una función (normalmente le llamamos onFormSubmit(e)) que se active al enviar el formulario y donde se recogen las respuestas más o menos así: var marca_temporal = e.values[0] var nombre = e.values[1] Pero, a veces, una hoja de cálculo tiene más de un formulario. ¿Cómo conseguimos las respuestas de uno y no de otro, o la de los dos? En este caso cada formulario está unido a una pestaña. Por lo que en el código aprovecharemos que esté esa pestaña activa para coger las respuestas del formulario unido a ella. La clave es getActiveSheet().getName(), es decir, comprobar el nombre de la pestaña que se activa al enviar el formulario unido a ella.
Este es el código:

function peticiones(e) {
// Creamos los enlaces a cada pestaña, tanto al nombre, como a la pestaña. El nombre nos servirá para hacer el condicional y la pestaña para conseguir datos que contenga.
var libro = SpreadsheetApp.getActiveSpreadsheet()


// En vez de utilizar GetSheetByName, utilizamos un nombre de rango para que, aunque se cambie el nombre de la pestaña, el código funcione
var nombre_peticiones =libro.getRangeByName('datos').getSheet().getName()
var nombre_soluciones =libro.getRangeByName('soluciones').getSheet().getName()
var datos = libro.getRangeByName('datos').getSheet()
var soluciones =libro.getRangeByName('soluciones').getSheet() 


// Gracias al nombre de la pestaña donde está cada formulario conseguimos los datos de uno o de otro

if (SpreadsheetApp.getActiveSheet().getName()==nombre_peticiones) {
  var temporal = e.values[0]
  var responsable = e.values[1]


              }

else if (SpreadsheetApp.getActiveSheet().getName()==nombre_soluciones)
              {
  var temporal = e.values[0]
  var codigo = e.values[1]


              } 

}

Presentación

Presentación

Este blog tiene como objetivo publicar trozos de código probados en archivos google drive que puedan servir a otras personas a no tener que repetir el esfuerzo de crearlo.
Además, estamos abiertos a que nos mandéis vuestras aportaciones, ya sea proponiendo otros códigos, como aconsejándonos formas de hacer código más corto y más eficaz. Ganaremos todos.
Suponemos que, si esta iniciativa tiene éxito, con el tiempo habremos creado un banco de código práctico y de calidad.
Es importante presentarnos también nosotros, para entender la orientación del código. Somos asesores de centros educativos públicos de Navarra orientados a la gestión de los centros. Intentamos crear herramientas de gestión para agilizar la gestión de dichos centros.

Crear PDF horizontal

El código que vamos a mostrar aquí no ha sido creado por nosotros, sino que lo hemos encontrado en Internet.

Objetivo: Publicar una pestaña de hoja de cálculo en PDF
Ventajas: El código permite crear el PDF con orientación horizontal, por lo que queda mucho más bonito y adecuado.
Características: El código te permite configurar varias opciones, además de la orientación de la página. Hemos metido un mensaje emergente para  que el usuario pueda elegir la pestaña y otro para que pueda configurar a que correo quiere que le llegue el PDF.

Este es el código:


function pdf_hor(){

// Creamos las variables de la hoja de cálculo
     var libro =SpreadsheetApp.getActiveSpreadsheet()
 // Pedimos la pestaña a convertir
    var nombre = Browser.inputBox('Dime la pestaña que quieres convertir en pdf')
  // Conseguimos la pestaña
    var pestana = libro.getSheetByName(nombre)
   // Creamos la URL con todas las variables de configuración
    var url = 'https://docs.google.com/spreadsheets/d/'+libro.getId()+'/export?exportFormat=pdf&format=pdf' // export as pdf / csv / xls / xlsx
  + '&size=A4'                           // paper size legal / letter / A4 // Elegimos el tamaño del papel
  + '&portrait=false'                     // orientation, false for landscape // Elegimos la orientación
  + '&fitw=true'                        // fit to page width, false for actual size // Elegimos si se extiende el contenido a toda la página
  + '&sheetnames=false&printtitle=false' // hide optional headers and footers // Elegimos si imprimimos cabecera con nombre de hoja
  + '&pagenumbers=false&gridlines=false' // hide page numbers and gridlines // Elegimos si aparecen número de página y líneas de celdas
  + '&fzr=false'                         // do not repeat row headers (frozen rows) on each page // Elegimos si repetimos primera línea en diferentes páginas
  + '&gid='+pestana.getSheetId();    // the sheet's Id // Si quitamos esta línea, conseguiremos que imprima todas las pestañas

  var token = ScriptApp.getOAuthToken();

  var response = UrlFetchApp.fetch(url, {
    headers: {
      'Authorization': 'Bearer ' +  token
    }
  });

  var theBlob = response.getBlob().setName(pestana.getName()+'.pdf');

  var newFile = DriveApp.createFile(theBlob);
  // Al mandar el correo, con la siguiente variable configuramos el mensaje
  var contenido_email= '<b>Aviso de mantenimiento</b>'
  var opciones= { htmlBody: contenido_email,  attachments: newFile};
  var correo = Browser.inputBox('Dime el correo a quien quieres mandar el pdf')
  // Mandamos el mensaje con el título "Mantenimiento". Lo podemos cambiar, pero siempre manteniendo las comillas
  MailApp.sendEmail(correo, 'Mantenimiento', contenido_email, opciones)
  // Borramos el PDF creado en nuestro Drive, para que no quede copia cada vez que mandamos el PDF por correo
  newFile.setTrashed(true)
}

Crear PDF horizontal

El código que vamos a mostrar aquí no ha sido creado por nosotros, sino que lo hemos encontrado en Internet. Objetivo: Publicar una pesta...