BAS#004 | Extrayendo los enlaces de edición de las respuestas de un formulario de Google con Apps Script

En estos momentos son las 18:53h de un tórrido domingo de mediados de julio y la temperatura 🌡🔥 ahí afuera se diría que se acerca a la de la superficie del planeta rojo, a pesar de que ya está bien entrada la tarde.

Refugiado en mi GAS-cueva ,en la que afortunadamente estoy fresquito y muy bien acompañado por post-rock del bueno 🎧, parece un buen momento para comenzar a escribir un nuevo Básicos Apps Script {BAS}.

Esta píldora BAS viene motivada por una consulta que me hizo llegar un compañero del equipo de coordinación de GEG Spain a finales del mes pasado:

¡Esto parece una tarea para Apps Script!

☝ Amigos, amigas, este es el tipo de desafío que se resuelve con Apps Script en un periquete... y de propina te da material para un BAS. ¡Seguid haciéndome llegar esas consultas!

¿Te apetece que resolvamos este problema juntos?

Es gratis. Bueno, casi, solo tienes que concederme tu atención unos minutejos.

En este artículo veremos cómo extraer las respuestas contenidas en un formulario de Google y los URL únicos que permiten su modificación desde un script asociado a una hoja de cálculo. Además, aprenderemos a manejar los posibles errores en tiempo de ejecución por medio de la potente estructura de control try / catch / finally.

TABLA DE CONTENIDOS

Descripción del problema

En este caso el procedimiento en sí es bastante claro, pero no por ello vamos a dejar pasar la oportunidad de definir con detalle los requerimientos, que nunca se sabe. Ya sabes, tan malo es resolver de manera errónea el problema correcto como de modo correcto el problema equivocado.

Volcando los URL de edición de las respuestas de un formulario de Google en una hoja de cálculo.

Nuestro objetivo es crear un pequeño script que alojado en una hoja de cálculo de Google que:

  1. Lea algunos parámetros de configuración introducidos por el usuario en ciertas celdas de configuración de dicha hoja de cálculo para especificar:
    • El  URL del formulario sobre el que debe trabajar.
    • Si debe extraerse o no la fecha de envío (marca de tiempo) de cada respuesta.
    • Si debe extraerse o no la dirección de correo electrónico de quien envía cada respuesta, suponiendo, claro está, que se estén recopilando. 
    • El número de preguntas, empezando a contar desde la primera definida en el formulario, cuyas respuestas también se extraerán (opcionalmente) para facilitar la contextualización de cada respuesta.
  2. Vuelque, a a partir de cierta fila de la hoja de cálculo, las respuestas recibidas junto a sus enlaces de edición, que es precisamente lo que perseguimos obtener.

Lo que no haremos en este BAS es enviar los enlaces a las personas interesadas para que modifiquen sus respuestas usando GAS, eso lo dejaremos para otra ocasión. En cualquier caso es algo que puede lograrse fácilmente usando, por ejemplo, el archiconocido complemento Form Mule sin necesidad de añadir para ello una sola línea de código a nuestro script de extracción.

¿Todo claro? Pues pongámonos a trabajar cuanto antes.

Preparativos (sin GAS)

Antes que nada, en esta carpeta encontrarás tanto la hoja de cálculo como el formulario de prueba que he utilizado para preparar esta píldora BAS. Ya sabes, el código está dentro de la hoja de cálculo, solo tienes que abrir el editor para manosearlo (Extensiones → Apps Script). También puedes revisarlo cómodamente en su repositorio GitHub, aquí.

📂 BAS#004 Obtener URL edición respuestas formulario 📂

📌 El formulario

Vamos a jugar con este sencillo formulario, que hemos configurado para recopilar el correo electrónico de las personas que lo responden. También dispone de dos campos identificativos adicionales (nombre y DNI) y de un par de preguntas que he puesto ahí simplemente por poner algo.

No, yo tampoco le encuentro  mucho sentido a esas preguntas.

📌 La hoja de cálculo

En esta hoja de cálculo es donde va a pasar todo...

Veamos qué hay aquí:

1️⃣ En esta celda escribiremos el URL del formulario cuyas respuestas deseamos obtener. Nos servirá su URL de edición, que puedes copiar de la barra de direcciones del navegador y pegar en la celda B1 de la hoja de cálculo.

2️⃣ En esta parte de la hoja de cálculo se han dispuesto unos controles para que el usuario pueda seleccionar qué elementos de cada una de las respuestas recibidas en el formulario han de ser obtenidos por el script de extracción.

Podremos importar entre 0 y 5 preguntas (las primeras) para facilitar la identificación de cada respuesta (Preguntas de ID). El script espera que estas preguntas existan en todas las respuestas del formulario dado que utilizará la primera respuesta recibida para identificarlas. De no ser así se producirán errores al ejecutarlo, cuidado por tanto si eliminas preguntas tras haber recogido ya alguna respuesta.

Además, será posible indicar si se desea extraer la fecha y hora en la que se ha recibido una respuesta, así como la dirección de correo electrónico de la persona que la ha enviado.

Verás que también se ha dispuesto una casilla de selección para el URL de edición de las respuestas del formulario, aunque no se puede desmarcar (por razones obvias, de eso va este BAS). A pesar de eso, el script está parametrizado de modo que se contempla esta posibilidad con el objetivo de hacerlo más flexible y facilitar su adaptación a otros casos de uso.

3️⃣ El botón que pondrá en marcha la extracción de información del formulario indicado. Al igual que en el BAS#003, aquí también evitaremos los menús personalizados.

4️⃣ Por último, en la tabla que ves en esta parte de la hoja de cálculo irá apareciendo la información obtenida del formulario, una fila por respuesta. Lo esperable.

Solución GAS

Inicializaciones

Como de costumbre, comenzamos con los ineludibles comentarios y algunas constantes, que el script usará para leer los ajustes de parametrización establecidos por el usuario en las celdas de la hoja de cálculo.

/** 
 * Este script extrae los URL de edición de las respuestas recibidas en el formulario indicado por
 * el usuario en la celda PARAMETROS.url, además de otra información adicional opcional. Todas las
 * respuestas deben tener el mismo nº de preguntas que se recuperan (celda numCampos).
 * 
 * Demo: https://drive.google.com/drive/folders/1jsduftDuOtSb3bmfGsbAwzXHMWidVcOn?usp=sharing
 * 
 * BAS#004 Copyright (C) 2022 Pablo Felip (@pfelipm)
 * 
 * Se distribuye bajo licencia MIT
 */
    
function resumirRespuestas() {

  // Constantes de parametrización del script
  const PARAMETROS = {
    filaTabla: 8,
    url: 'B1',
    numCampos: 'B3',
    fechaSiNo: 'B4',
    emailSiNo: 'B5',
    urlSiNo: 'B6'
  };

  // Hoja de cálculo
  const hdc = SpreadsheetApp.getActive();
  const hoja = hdc.getActiveSheet();

  // Leer parámetros
  const numCampos = hoja.getRange(PARAMETROS.numCampos).getValue();
  const fechaSiNo = hoja.getRange(PARAMETROS.fechaSiNo).getValue();
  const emailSiNo = hoja.getRange(PARAMETROS.emailSiNo).getValue();
  // Sí, este parámetros es 'fake', dado que la la hdc no permite desmarcar la casilla, pero ahí queda
  const urlSiNo = hoja.getRange(PARAMETROS.urlSiNo).getValue();

Cacemos excepciones con try / catch / finally

Antes de abordar la implementación de la sección principal del código de nuestro script, dediquemos unos minutos a explicar para qué sirve la estructura de control try → catch → finally.

Ya te hablé muy brevemente de ella en los apartados finales tanto del BAS#001 como del BAS#002, aunque en ambas ocasiones me dejé el finally por aquello de no marear. Tal vez quieras revisarlos rápidamente.

Y es que a nuestros scripts les puede pasar de todo mientras se están ejecutando: 

  • Tal vez su lógica no haya tenido en cuenta cierta combinación de circunstancias que provoca errores al utilizar determinados métodos de los servicios de Google o invocar otras APIs.
  • Quizás las llamadas a esos servicios y APIs simplemente fallen, puede que por cuestiones relacionadas con esos molestos (pero necesarios) límites de servicio, puede que debido a incidencias transitorias sobre las que no tenemos control alguno.

Estas circunstancias fatales harán que nuestro script se detenga inmediatamente, mostrando una fea advertencia que seguramente dejará al sufrido usuario confundido y con cara de póker. Ese usuario puedes no ser tú, así que hazme al favor de ponerle las cosas fáciles. Me refiero a mensajes de error como este:

En la mayoría de los casos no vamos a necesitar un mensaje específico para todas y cada una de las situaciones de error particular que pudiera darse, pero sí al menos los necesarios para cubrir las más habituales, agrupando el resto en otro más genérico. Ante un error, nuestros scripts deben facilitar información significativa acerca de la naturaleza del problema que se ha producido y accionable, es decir, que oriente al usuario acerca de cómo debe actuar, por ejemplo, intentándolo de nuevo más tarde, contactando con un técnico de soporte, etc.

Además, cuando un proceso se ve interrumpido de manera inesperada, pueden ser necesarias acciones adicionales para controlar los daños.

Me explico.

Supongamos, en el caso que nos ocupa, que nuestro script borra en cuantocomienza su ejecución las filas que contienen las respuestas ya obtenidas anteriormente, indicando así de manera visible que un nuevo intento de recuperación de respuestas ha dado comienzo.

En caso de que se produjera un error inesperado que impidiera completar el proceso, el usuario se quedaría en medio de ninguna parte, no dispondría de las respuestas del formulario que necesita, pero tampoco conservaría en la hoja de cálculo las anteriores y tendría que tirar del comando deshacer o del historial de versiones de la hoja de cálculo para tratar de recuperarlas 🙏.

¿No te parece que resultaría mucho más elegante enriquecer nuestros scripts con el código necesario para gestionar estos imponderables?

En el caso descrito hace un instante bastaría con almacenar temporalmente las respuestas ya obtenidas previamente para volverlas a colocar en su lugar en caso de que se produjera una excepción que impidiera completar la ejecución sin incidentes.

Pretendemos, en definitiva, que nuestros scripts sean resilientes y puedan sobreponerse a los errores inesperados, sin detener su ejecución.

Resumiendo, se trata de capturar las excepciones y tratarlas de manera controlada de manera que:

🗨 El script muestre mensajes de error que resulten significativos para los seres humanos con los que interacciona.

🚨 El script falle graciosamente, minimizando en la medida de lo posible las consecuencias indeseables derivadas de un proceso que no se haya podido desarrollar del modo previsto.

¿Pero cómo se traslada todo esta parrafada a nuestros scripts con try / catch / finally? Veámoslo con un sencillo esquema. Fíjate en los tres bloques claramente delimitados en las líneas 1 - 5, 6 - 10 y 11 - 16:

Cazando excepciones en Apps Script.

☝ Debes saber que los bloques catch y finally son opcionales, pero tiene que haber siempre al menos uno de ellos tras un catch. No vamos a hablar en esta píldora BAS de los matices, que los hay, así como tampoco acerca del anidamiento de estos bloques de gestión de errores. Todo eso otro día, tal vez.

Por cierto, el identificador  e que ves ahí arriba, en la declaración del bloque catch, es típicamente un objeto de la clase Error de JavaScript, susceptible de ser interrogado para tratar de averiguar qué demonios ha pasado, es decir, para obtener más información acerca de las circunstancias que han desencadenado la excepción. Veremos cómo usarlo, así en plan básico y de manera no demasiado rigurosa, en un momento.

🤷‍♂️ Aunque por razones que ni yo mismo entiendo tengo la maldita costumbre de escribir catch(e), lo correcto es catch (e), con un espacio entre ambos elementos, dado que catch no es un método sino una palabra clave en JavaScript (como if, for o while, por ejemplo). ¡Prometo enmendarme a partir del próximo BAS!

¿Y si acabamos antes de empezar?

Tras las inicializaciones, vamos con la parte del script responsable de la extracción de datos del formulario, que meteremos dentro de un  try {...}.

Lo primero es acceder al formulario y verificar que contenga alguna respuesta usando el método getResponses() de la clase Form del servicio de formularios, que devolverá un vector (array) de objetos de tipo FormResponse.  Guardaremos este vector  en respuestas

  // Sección principal, que se ejecuta dentro de un bloque en el que
  // se cazarán los errores en tiempo de ejecución.
  try {

    // Acceder al formulario objetivo y verificar si hay respuestas
    const formulario = FormApp.openByUrl(hoja.getRange(PARAMETROS.url).getValue());
    const respuestas = formulario.getResponses();
    
    if (respuestas.length == 0) throw 'No hay respuestas en el formulario.'

Si el número de elementos de respuestas es 0 (se consulta sin más su propiedad length) ya no va a ser necesario continuar. Lógico.

Pero espera un momento, ¿qué demonios es eso de throw que has puesto ahí, en la línea 44? ¿Alguien va a vomitar?

Probablemente esperabas en su lugar algo como esto, ¿verdad?

Comprobando si hay respuestas... del modo clásico.

Calma, hasta el final de Aniquilación, tiene explicación. Bueno, igual eso no.

El operador throw de JavaScript nos permite provocar una excepción. Y podemos lanzar con ella cualquier cosa: valores numéricos, cadenas de texto, booleanos... y muy a menudo, objetos.

Sí, lo has leído bien. Vamos a generar un error a propósito

Eso provocará que, de manera inmediata y sin pasar por la casilla de salida (referencia viejuna para gente que hizo la EGB, si no la pillas no tienes aún de qué preocuparte 😉), se pase a ejecutar el código dentro del bloque catch, que hemos puesto ahí precisamente para cazar las posibles excepciones. De este modo evitaremos drásticamente, en nuestro caso de uso, que se traten de recuperar las respuestas del formulario.

Justo lo que pretendíamos ✌.

¿Y por qué no usar un if ... else de-toda-la-vida?

Pues no es por maldad, veas tú, sino porque quería mostrarte este sugerente patrón, aún desconocido para nosotros en {BAS}, dado que en determinadas circunstancias puede mejorar la legibilidad de tu código.

¿Pero qué circunstancias son esas?

Supón que antes de efectuar un proceso determinado tu código necesitara realizar no una sino múltiples comprobaciones de manera secuencial. Pongamos que tres, venga, por no abusar.

En plan clasicote, la cosa podría ir así (quito ahora las líneas en blanco del fragmento de código que se viene porque de lo contrario se me desmadra un poco la altura de la imagen):

Mi TOC ruge como un león cabreado ante ese código tan escalonado.

Vamos a ver, hacemos una primera comprobación, si es que sí seguimos adelante, hacemos otra, luego comprobamos por tercera vez  y solo si esta última también se cumple hacemos lo que tenemos que hacer (el proceso es el proceso).

Y lógicamente vamos cerrando cada una de esas comprobaciones con otros tantos else que desencadenan ciertas acciones (emitir mensajes informativos) solo en caso de que no se satisfagan sus condiciones respectivas.

Si con tres ya queda feo, con más ni te cuento.

En cambio, con nuestro nuevo patrón, la cosa quedaría así de compacta. 

Lanzando excepciones a lo loco. Fíjate en que ahora las condiciones están negadas.

Dentro del catch(e) {...} determinaremos por qué razón hemos llegado ahí comprobando  el valor de  e y emitiremos el mensaje de error que corresponda en cada caso.

¿Quiero esto decir que ya siempre tienes que optar por hacer las cosas de este modo?

En algunas ocasiones es posible que la secuencia de decisión a la que te enfrentes permita efectuar todas las comprobaciones necesarias de una vez, esto es, usando un solo if. De libro, vamos.

¡Esto no es siempre posible!

Con frecuencia también será posible resolver la papeleta usando una limpia estructura de decisión if ... else if, facilitando así el tratamiento individualizado de cada condición,  pero evitando al mismo tiempo engordar la bestia temible de la indentación hasta que no quepa por la puerta.

La respuesta a la pregunta anterior, por tanto, es depende.

En cualquier caso, ahora ya dispones de más recursos con los que enfrentarte a tus demonios algorítmicos condicionales.

⚠️ En los ejemplos anteriores me he tomado la licencia de usar de un modo no demasiado ortodoxo la sentencia throw. En lugar de lanzar objetos, con sus propiedades y tal, estoy usando simples cadenas de texto. Otro día si quieres hablamos de objetos, constructores y esas cosas.

Informando del inicio del proceso

Esto va bien, ya estamos en camino...

Lo primero que haremos ahora es señalizar de algún modo que el proceso va a iniciarse. Si en el BAS#003 escribíamos un simple mensaje informativo dentro de una celda de la hoja de cálculo, en esta ocasión haremos algo más elegante.

    // Señalizar inicio del proceso de extracción de respuestas
    hdc.toast('Obteniendo respuestas...', '', -1);

    // Posibles datos anteriores en gris claro durante el proceso
    let ultimaFila = hoja.getLastRow();
    if(ultimaFila > PARAMETROS.filaTabla) {
      hoja.getRange(PARAMETROS.filaTabla + 1,1, ultimaFila - PARAMETROS.filaTabla + 1, hoja.getLastColumn()).setFontColor('#d0d0d0');
      SpreadsheetApp.flush();
    }

1️⃣ Por un lado, usaremos en la línea 47 el método toast() de la clase Spreadsheet del servicio de hojas de cálculo para mostrar un pequeño recuadro emergente con un mensaje informativo en el extremo inferior derecho de la ventana de la hoja de cálculo. Su tercer parámetro es opcional y representa el tiempo, en milisegundos, durante el que debe debe mostrarse el mensaje. Si no se especifica, por defecto se muestra durante 5 segundos. Un valor negativo supone hacerlo indefinidamente, hasta que el usuario haga clic sobre él o lancemos otro toast.

Una notificación buena, bonita y barata... solo para hojas de cálculo, eso sí.

Esto lo vas a ver mucho en scripts que funcionan sobre hojas de cálculo dado que constituye un método sencillo y visualmente poco intrusivo de contarle al usuario qué está pasando.

☝ Atención, toast solo está disponible en scripts asociados a hojas de cálculo de Google. Si tus scripts funcionan sobre documentos, presentaciones o formularios tendrás que recurrir a otras artimañas para comunicarte con el ser humano al otro lado de la pantalla.

2️⃣ Por otro lado, haremos que el texto de las celdas que contienen posibles respuestas previas ya obtenidas  (sin incluir a la fila de encabezado) pase a ser de un color gris claro, dando a entender que esa información "está en el aire", como suele decirse (líneas 50 - 54).

Un ¿sutil? modo de indicar que la información en la tabla es provisional.

Con getLastRow() se obtiene la posición (empezando a contar desde 1) de la última fila de la hoja de datos de trabajo (constante hoja) que contiene alguna celda no vacía. Si se encuentra por debajo de la fila de encabezado (constante PARAMETROS.filaTabla) eso querrá decir que la tabla ya contiene respuestas resultantes de una extracción anterior, cuyo texto ahora colorearemos de gris con el método setFontColor() de la, ya conocida en estas píldoras BAS, clase Range. El número de columnas del intervalo afectado se obtiene de manera análoga con getLastColumn()El color se especifica como un valor de texto #d0d0d0 en formato hexadecimal. 

☝ No confundas los métodos getLastRow() y getLastColumn() con sus homólogos aparentemente similares getMaxRows() y getMaxColumns(). Parecen casi lo mismo, pero en tanto que los primeros nos proporcionan la posición de la última fila o columna que contiene datos, los segundos nos devuelven el número total de filas o columnas en la hoja, estén vacías o no.

Por último (en esta parte del  script), nos aseguraremos de que el cambio de color del texto sea visible inmediatamente pidiéndole amablemente al servicio de hojas de cálculo que aplique los cambios inmediatamente por medio del método flush() de la clase SpreadsheetApp (línea 53).

Normalmente dejaremos que este servicio refresque la hoja de cálculo cuando considere oportuno por una cuestión de optimización, pero en esta ocasión le meteremos presión porque necesitamos asegurarnos de que esto se haga ipso facto para que nuestra eso de cambiar el color de algunas celdas tenga sentido.

Prosigamos.

La extracción de respuestas

Ahora toca meterle mano a las respuestas del formulario. Vamos a construir una tabla en la que guardaremos toda la información que obtengamos de él.

Lo primero es montar la fila de encabezado. Lo haremos sobre el vector encabezados, que podrá contener las etiquetas de las columnas de:

  • 🗓️  Marca de tiempo (fecha y hora).
  • 📨 Dirección de correo electrónico.
  • 🔤 Las respuestas de hasta cinco preguntas de identificación.
  • ✍️ El URL de edición de la respuesta.

Esto dependerá de la información introducida por el usuario en las celdas B4:B6, que se han leído previamente en las constantes fechaSiNo, emailSiNo y urlSiNo, como te he mostrado en un fragmento de código previo.

    // Generar la fila de encabezado de la tabla de respuestas
    const encabezados = [];
    if (fechaSiNo) encabezados.push('🗓️ Marca tiempo');
    if (emailSiNo) encabezados.push('📨 Email');
    // Si numCampos > nº respuestas se toman todas las disponibles
    respuestas[0].getItemResponses().slice(0, numCampos).forEach(item => encabezados.push(item.getItem().getTitle()));
    if (urlSiNo) encabezados.push('✍️ URL edición');

Como puedes ver, técnicamente la columna con los URL de edición de respuestas también es un elemento opcional, aunque nuestra cutreinterfaz de usuario no permita desmarcar la casilla que selecciona este elemento (ya hemos hablado de ello hace un rato).

La instrucción encadenada que hay en la línea 61 es posiblemente durilla. Desmontémosla pieza a pieza:

  1. El elemento respuestas[0] representa la primera respuesta contenida en el formulario. Y ya sabemos que debe haber al menos una porque nos hemos asegurado de ello en la línea 44.
  2. Usamos el método getItemResponses() para obtener un array de objetos de tipo ItemResponse. Esto se suele representar como ItemResponse[], es decir, un array de objetos ItemResponse. En su interior encontraremos toda la información almacenada en el formulario de cada una de las preguntas que forman parte de la respuesta, incluyendo, por descontado, su enunciado.
  3. A continuación hacemos slice(0, numCampos), que ya utilizamos en el BAS#003, para quedarnos solo con un vector que contendrá las numCampos primeras preguntas, de acuerdo con el valor numérico introducido por el usuario en la celda B3.
  4. Después recorremos el vector de preguntas por medio de un forEach y añadimos al vector encabezados el título (enunciado) de la pregunta. Para lograrlo, primero usamos el método getItem() sobre el objeto ItemReponse para seguidamente obtener por fin el dichoso título de la pregunta haciendo getTitle() sobre el objeto de la clase Item devuelto.

😵‍💫 Si tanto método y objeto te ha dejado el cuerpo como si hubieras montado en el Dragon Khan mirando en sentido contrario a la marcha, tómate una aspirina (o mejor, una Alhambra verde o tostada 🍺) y síguele la pista a lo que pasa en esa línea de código diabólica leyendo detenidamente la documentación de ayuda que te he enlazado en los párrafos anteriores. También puedes repasar los BAS anteriores, especialmente el apartado 2.2 del BAS#001, en el que te contaba cosillas básicas sobre objetos y métodos.

Ya casi lo tenemos. Vamos ahora a por las respuestas. Las conseguiremos con este puñado de líneas:

    // Extraer respuestas
    const datos = respuestas.map(respuesta => {

      const filaDatos = [];
      if (fechaSiNo) filaDatos.push(respuesta.getTimestamp());
      if (emailSiNo) filaDatos.push(respuesta.getRespondentEmail());
      respuesta.getItemResponses().slice(0, numCampos).forEach(item => {
          // getResponse() puede devolver String | String[] | String[][], aplanaremos los arrays y generaremos una cadena de texto
          // con valores separados por comas, ver https://developers.google.com/apps-script/reference/forms/item-response#getresponse
        filaDatos.push(
          Array.isArray(item.getResponse())
          ? item.getResponse().flat().join(', ')
          : item.getResponse());
      });
      if (urlSiNo) filaDatos.push(respuesta.getEditResponseUrl());
      return filaDatos;
    
    });
 

Algunos comentarios pertinentes, que aquí hay cosas que no han salido en píldoras BAS anteriores.

Primero y principal, no te pierdas ese intrigante  map en la línea 65. Si no me falla la memoria, es la primera vez que lo usamos en un BAS. Se trata de un método de JavaScript, formalmente denominado Array.prototype.map(). Ahora ya os conocéis.

¿Te acuerdas de los métodos de lista forEach, filter y some? Hablamos por primera vez de ellos en el BAS#001 ¡otra vez!. Pues map es otro de ellos, osea, más de lo mismo, con algunos matices.

Resulta que map es muy similar a forEach. Ambos ejecutan una serie de instrucciones sobre todos los elementos de un vector. Pero map además crea un nuevo vector al vuelo, de exactamente el mismo tamaño, y escribe en él los valores que devuelve, es decir, los resultantes de realizar una serie de operaciones sobre cada uno de los elementos del vector original.

Fíjate, en el bloque de código entre las líneas 67 y 78 vamos metiendo con push cosas misteriosas (las destripamos en nada) en el vector filaDatos, cosas que básicamente se obtienen a partir de la aplicación de una serie de métodos sobre la variable respuesta, que como seguro que has adivinado ya contiene un objeto de la clase FormResponse.

Seguro que ya lo estás viendo venir, pero para asegurarme te lo resumo:

1️⃣ La instrucción map itera en bucle sobre todos y cada uno de los elementos del vector respuestas usando la variable respuesta. Que sí, parecido a forEach.

2️⃣ Todo el código entre corchetes (he dejado unas campechanas líneas en blanco antes y después de él para que lo veas clarinete) trabaja con esa variable denominada respuesta.

3️⃣ Finalmente, en la línea 79, se devuelve la variable filaDatos, que se ha ido montando dentro del buble, con un glorioso y definitivo:

 return filaDatos; 

El vector que se produce en cada iteración es empujando tras la ejecución del bucle map dentro del array datos, que por tanto presentará una estructura de matriz bidimensional de elementos. 

Map explicado con cuatro rayas.

Esencialmente, lo que estamos haciendo es ejecutar el código dentro de map de manera parametrizada para construir, fila a fila, una estructura con la información que se extrae de las respuestas del formulario sobre la variable datos, que posteriormente volcaremos en la tabla de respuestas de la hoja de cálculo. 

LíneaObtieneMétodos
68La fecha y hora en la que se envío la respuesta.getTimestamp()
69La dirección de correo electrónico de la persona que envió la respuesta, si es que existe. Si en el momento del envío el formulario no estaba configurado para registrar las direcciones de correo este método simplemente devolverá una cadena de texto vacía.getRespondentEmail()
70 - 77Las respuestas a las preguntas de identificación seleccionadas.¡Unos cuantos, los vemos a continuación!
78Nuestro ansiado objetivo, el URL que permite editar cada respuesta.getEditResponseUrl()

Como puedes intuir, la parte más perra de todo esto está en las línea 70 - 77, y para entenderla totalmente no tendrás más remedio que bucear en la documentación de la clase FormResponse y de todo lo que viene detrás (o dentro, según se mire), de ella:

1️⃣ En la línea 70 obtenemos el vector de preguntas de cada respuesta usando getItemResponses() .

2️⃣ Sin salir de esa misma línea, y de un modo análogo a lo que hacíamos para extraer los títulos de las preguntas, tiramos de slice para quedarnos solo con las preguntas de identificación seleccionadas.

3️⃣ A continuación, recorremos el vector de objetos de tipo ItemResponse con un ya familiar forEach.

4️⃣ Por fin obtenemos la respuesta. Pero cuidado, tal y como advierte la documentación de getResponse(), el valor que devuelve este método puede ser un cadena de texto String, un vector de cadenas String[] o incluso una matriz bidimensional de cadenas  String[][]. Esto dependerá del tipo de la pregunta  (texto, cuadrícula de varias opciones, cuadrícula de casillas...).

Pero necesitaremos valores únicos que guardar en cada elemento de nuestro vector - fila. Para ello, verificaremos primeramente si la respuesta que devuelve este método es un array por medio del método Array.isArray(). En caso de que lo sea:

  1. Lo aplanaremos con un rotundo Array.prototype.flat(), que lo dejará compactado en una única y conveniente dimensión. Si aplicamos flat() sobre un vector de una sola dimensión el resultado es el mismo vector, así que no será necesario realizar ninguna comprobación previa adicional.
  2. Generaremos una cadena de texto con todos sus elementos separados por coma + espacio mediante Array.prototype.join().

☝ En lugar de utilizar un sentencia if verás que se ha optado en su lugar por usar el operador condicional ternario, con su escueta estructura condición ? valor_1 : valor_2.  No conviene abusar de él porque puede empeorar en un plis la legibilidad del código, pero en determinadas situaciones facilita la toma de decisiones de manera compacta. Y sí, hay una cierta controversia por lo que hace a dónde colocar los símbolos ? y : cuando este operador se dispone en varias líneas. Para gustos, colores.

Decisión y acción...
...o acción parametrizada condicionalmente. ¡Tú eliges!

Volcando las respuestas y... ¡fin del proceso!

Y ya lo que queda es pan comido...

    // Montar encabezado y respuestas en una sola tabla
    const tabla = [encabezados, ...datos];
       
    // Escribir tabla en la hoja de cálculo, borrando datos previos, si los hay
    ultimaFila = hoja.getLastRow();
    if (ultimaFila > PARAMETROS.filaTabla) {
      hoja.getRange(PARAMETROS.filaTabla, 1, ultimaFila - PARAMETROS.filaTabla + 1, hoja.getLastColumn()).clearContent();
    }
    hoja.getRange(PARAMETROS.filaTabla, 1, tabla.length, tabla[0].length).setValues(tabla);

    // Informar del fin del proceso (con éxito)
    hdc.toast(`Respuestas obtenidas: ${tabla.length - 1}.`, '');
    

Primero, pegamos encabezados y respuestas (arrays encabezados y datos) en la línea 84).  Si no entiendes cómo es posible hacer eso con una instrucción tan corta... al BAS#001 que te vas de cabeza otra vez (apartado 2.3). En el te presentaba el operador de propagación, que igual sirve para un roto que para un descosido.

A continuación (líneas 87 - 91), se escribe el contenido de la matriz tabla en la hoja de cálculo, borrando previamente las celdas con el método clearContents() si es necesario.

Por último, en la línea 94 lanzaremos un certero toast de cierre para avisar al usuario de que todo ha terminado. Y de propina le diremos cuántas respuestas se han obtenido usando la sintaxis de plantillas literales de JavaScript. ¿Qué tampoco sabes de que va eso? El apartado 2.2 del BAS#002 tienes todas las respuestas que buscas.

¡Pero no se vayan todavía, aún quedan catch y finally!

Igual te habías olvidado de ellos. No debes hacerlo, hay que terminar siempre como es debido lo que se ha comenzado.

Veamos primero ese catch.

  } catch(e) {
    // Informar de error, si el objeto e es de tipo string es porque hemos llegado
    // aquí al fallar la comprobación de existencia de respuestas (¡sucio!).
    hdc.toast(typeof e == 'string' ? e : `Error interno: ${e.message}`, 'No hay respuestas en el formulario');
    

Cuando se produce un error totalmente inesperado mientras el script está corriendo, esto es, una excepción que no hemos provocado nosotros mismos lanzando un throw, el parámetro e que recibe el manejador de error será un objeto que el motor de ejecución de JavaScript diligentemente nos facilita.

Por tanto, lo que haremos aquí es comprobar si e es de tipo cadena (string). Eso querrá decir que hemos llegado hasta este bloque como consecuencia del throw que usamos unas decenas de líneas más arriba para abortar la ejecución en caso de que el formulario no contuviera respuestas.

De ser así, mensaje informativo específico al canto y a correr.

Si en cambio se trata de una excepción externa, no nos quedará otra que mostrar el mensaje de error que nos pasa JavaScript en la propiedad message del objeto de error. Y que el sufrido usuario se las apañe, que para eso son su hoja de cálculo y su formulario.

La comprobación la haremos con el espero que ya entrañable a estas alturas operador de comparación compacto ?, que ataca de nuevo, y el operador de JavaScript typeof, que por su parte devuelve una valor de texto que representa el tipo del operando facilitado. ¡Ojito que ya llevamos unos cuantos operadores a cuestas!

¿Y que hay del bloque finally?

Pues poca cosa, pero importante.

  } finally {
    // Esto se ejecuta siempre, tanto si hemos cazado algún error como si todo ha ido ok,
    // contenido de la tabla en color habitual.
    ultimaFila = hoja.getLastRow();
    if (ultimaFila > PARAMETROS.filaTabla) {
      hoja.getRange(PARAMETROS.filaTabla + 1,1, ultimaFila - PARAMETROS.filaTabla + 1, hoja.getLastColumn()).setFontColor(null);
    }
  }
}

Ya sabes que esta parte se va a ejecutar tanto si todo ha ido bien como si se ha producido alguna excepción, así que aprovecharemos para volver a pintar con el color por defecto, gracias a un certero setFontColor(null), el texto de la tabla de respuestas. ¡Apuesto a que ya te habías olvidado de que lo habíamos dejado de un gris inmaterial! No pasa nada, para eso estamos.

Un BAS no sería lo mismo sin un GIF demostrativo (te confieso que he estado a puntito de olvidarme de él). Aquí lo tienes.

¡BAS#004 en acción!

Siguientes pasos

Tengo que decir que me ha quedado un BAS más extenso de lo que yo esperaba. El contador que tengo aquí en el editor dice que leerlo te va llevar 30 minutos, aunque no sé si creermelo. Por esa razón no me voy a enrollar mucho más.

Me dejo a sabiendas cosas en el tintero, por ejemplo explicarte la diferencia entre el uso de const y let a la hora de declarar variables (en la línea 50 encontrarás una de estas últimas).

Tampoco hemos profundizado demasiado en los parámetros adicionales que soporta map (y también sus coleguillas forEach, filter, some y compañía), parámetros que pueden resultar de enorme utilidad en algunas situaciones, ni tampoco hemos hablado de su versión corta, que no precisa de una sentencia return.

Pero me resisto a despedirme sin proponerte al menos una mejora.

Al recolectar las respuestas a las preguntas de identificación nos hemos asegurado de obtener siempre una cadena de texto con elementos separados por comas. Esto nos ha resuelto la papeleta a la hora de escribirlas en las celdas una hoja de cálculo, pero corremos el riesgo de obtener información ambigua.

Por ejemplo, si en una pregunta de tipo cuadrícula de casillas se ha respondido esto:

Una respuesta ligeramente puñetera.

En la hoja de cálculo la respuesta aparecerá como T1, T2, T3, sin que tengamos forma humana de determinar con seguridad a qué opción (fila) se corresponden T2 y T3.

¿Cómo podríamos evitarlo?

🕵️‍♀️ Pista: hay que tocar las líneas 73 - 76. Y ahí lo dejo 😏.

Pero he dicho que iba a ser breve, así que ya está bien por hoy. Ya sabes dónde tienes todas las píldoras BAS y el repositorio de código.

Como de costumbre, no dudes en dejar tus consultas y aportaciones en la sección de comentarios de este artículo o en el canal #gas-iniciación de Apps Script Ñ.