
Velocidad vs. Permisos: Ordenando pestañas de hojas de cálculo con Apps Script
¿Hay algo mejor en el mundo que un artículo sobre hojas de cálculo de Google? Solo se me ocurre una cosa: una artículo sobre hojas de cálculo de Google en el que además se hable de Apps Script 😏.
Por eso hoy...
Analizaremos la implementación y compararemos las ventajas y desventajas de usar el servicio integrado Apps Script frente a la API de Google Sheets para ordenar las pestañas de una hoja de cálculo.
TABLA DE CONTENIDOS
Definamos el problema
Partimos de una hoja de cálculo de Google (libro de cálculo, si lo prefieres) que contiene un número arbitrario de hojas (pestañas).

Nuestro objetivo es construir una función Apps Script que las ordene alfabéticamente. Estableceremos además los siguientes requerimientos:
- La ordenación puede ser ascendente o descendente.
- La ordenación debe respetar las reglas del idioma configurado en la hoja de cálculo, manejando correctamente acentos, mayúsculas y minúsculas, así como los números que pudieran aparecer en el nombre de cada hoja (por ejemplo, «Hoja 10» debe aparecer después de «Hoja 2» al ordenar de manera ascendente).
- Se deben ordenar todas las hojas existentes, incluyendo las ocultas.
- Al finalizar el proceso, la hoja activa debe ser la misma que había en el momento de iniciarlo.
- Se debe preservar el estado de visibilidad de las hojas.
- El código debe ser razonablemente eficiente.
Encontrarás todo el código que vamos a desarrollar en esta hoja de cálculo:
Y también en este pequeño repositorio GitHub.
Ordenando con el servicio integrado de hojas de cálculo
Comenzaremos con el método más sencillo y directo: utilizar la clase SpreadsheetApp del servicio de hojas de cálculo de Google Apps Script.
Antes de abordar el código de nuestro primer intento, desplegaremos un menú personalizado que nos permitirá ejecutar cómodamente cada una de las las funciones que vamos a desarrollar a lo largo de este artículo.
Archivo «General.gs»
/**
* Código de acompañamiento del artículo:
* «Velocidad vs. Permisos: Ordenando pestañas de hojas de cálculo con Apps Script»
* https://pablofelip.online/velocidad-permisos-ordenando-pestanas-apps-script
* Pablo Felip Monferrer | 2025
*/
function onOpen() {
SpreadsheetApp.getUi().createMenu('Ordenar hojas')
.addItem('🐢 Ordenar hojas (A → Z) [SERVICIO]', 'ordenarHojasServicioAsc')
.addItem('🐢 Ordenar hojas (Z → A) [SERVICIO]', 'ordenarHojasServicioDesc')
.addSeparator()
.addItem('🔀 Desordenar hojas [SERVICIO]', 'desordenarHojasServicio')
.addSeparator()
.addItem('⚡ Ordenar hojas (A → Z) [API]', 'ordenarHojasApiAsc')
.addItem('⚡ Ordenar hojas (Z → A) [API]', 'ordenarHojasApiDesc')
.addToUi();
}
// Envoltorios para ordenarHojas()
function ordenarHojasServicioAsc() { ordenarHojasServicio(true); }
function ordenarHojasServicioDesc() { ordenarHojasServicio(false); }
function ordenarHojasApiAsc() { ordenarHojasApi(true); }
function ordenarHojasApiDesc() { ordenarHojasApi(false); }
Para no complicarnos la vida innecesariamente enredando con el objeto global de JavaScript (JS en adelante), utilizaremos sencillas funciones auxiliares a modo de envoltorios que llamarán a las que realmente realizan la ordenación seleccionando el modo, ascendente o descendente, deseado.
Pasemos a lo importante, la estrategia de ordenación de hojas, que no está exento de ciertas peculiaridades.
El servicio integrado de hojas de cálculo —a mí me gusta hablar de servicios básicos— nos proporciona exclusivamente el método moveActiveSheet para establecer la posición de una hoja en la lista de hojas por medio de su argumento único pos.

Este método se utiliza sobre un objeto de tipo Spreadsheet, lo que tiene bastante sentido dado que se trata de alterar la estructura de la hoja de cálculo, pero opera de manera inamovible sobre la hoja activa.
Esto supone que, dado que el servicio básico no cuenta con otro mecanismo más flexible para efectuar esta operación, nos veremos obligados a:
- Recorrer la lista de hojas a ordenar.
- Activar cada una de ellas.
- Inmediatamente a continuación, mover la hoja activa a su nueva posición, de acuerdo con el criterio de ordenación establecido.
Adicionalmente, al establecer como activa cualquier hoja oculta esta se hará visible, por lo que tendremos que gestionar esta circunstancia para que nuestra función de ordenación preserve la visibilidad del conjunto de hojas, de acuerdo con las directrices de diseño establecidas, y además finalice mostrando la misma hoja que teníamos en pantalla antes de iniciar el proceso.
Ahora sí, aquí puedes ver el código de nuestra primera función ordenarHojasServicio.
Archivo «Ordenar (servicio).gs»
/**
* Ordena las hojas de un libro de cálculo alfabéticamente en sentido ascendente o descendente
* utilizando el servicio integrado de hojas de cálculo.
* @param {boolean} [ascendente]
*/
function ordenarHojasServicio(ascendente = true) {
const hdc = SpreadsheetApp.getActiveSpreadsheet();
const ui = SpreadsheetApp.getUi();
// Precisamos únicamente el código del idioma (idioma_región)
const collator = new Intl.Collator(hdc.getSpreadsheetLocale().split('_')[0], { numeric: true, sensitivity: 'base' });
try {
// Vector { hoja, nombre } para evitar múltiples llamadas a getName() en la función del sort().
const hojasOrdenadas = hdc.getSheets()
.map(hoja => ({ objeto: hoja, nombre: hoja.getName() }))
.sort((hoja1, hoja2) => Math.pow(-1, !ascendente) * collator.compare(hoja1.nombre, hoja2.nombre));
const hojasPorOcultar = [];
const hojaActual = hdc.getActiveSheet();
// Identifica las hojas ocultas para dejarlas del mismo modo tras realizar la ordenación
hojasOrdenadas.forEach((hoja, pos) => {
if (hoja.objeto.isSheetHidden()) hojasPorOcultar.push(hoja.objeto);
hdc.setActiveSheet(hoja.objeto);
hdc.moveActiveSheet(pos + 1);
});
// Necesario confirmar cambios antes de procesar hojas a ocultar
SpreadsheetApp.flush();
// Sin esta pausa en ocasiones alguna hoja oculta queda visible
Utilities.sleep(1000);
if (hojasPorOcultar.length > 0) hojasPorOcultar.forEach(hoja => hoja.hideSheet())
hdc.setActiveSheet(hojaActual);
SpreadsheetApp.flush();
ui.alert(`Se han ordenado alfabéticamente ${hojasOrdenadas.length} hoja(s) en sentido ${ascendente ? 'ascendente' : 'descendente'}.`, ui.ButtonSet.OK);
} catch (e) {
ui.alert(`Se ha producido un error inesperado al ordenar las hojas, inténtalo de nuevo.
⚠️ ${e.message}`, ui.ButtonSet.OK);
}
}
¿Sacamos el bisturí?
Como puedes ver en la línea 6, la función únicamente cuenta con el parámetro ascendente, que de manera predeterminada adoptará un valor lógico verdadero. Recuerda que está función será invocada por las funciones auxiliares ordenarHojasServicioAsc y ordenarHojasServicioDesc, que le facilitarán el argumento apropiado en cada caso de acuerdo con el comando seleccionado en el menú personalizado.
En la línea 11 se instancia un objeto JS de tipo Intl.Collator, que será quien se encargará de comparar alfabéticamente los nombres de las hojas de acuerdo con las convenciones propias del idioma configurado en la hoja cálculo.
El código ISO del idioma se obtiene troceando la secuencia de texto que representa la configuración regional devuelta por el método getSpreadSheetLocale mediante el sencillo pero efectivo método JS split, que se queda con la subcadena previa al guión bajo [_
].
hdc.getSpreadsheetLocale().split('_')[0]

Por otra parte, las opciones indicas al crear el objeto Collator posibilitan una ordenación correcta de las secuencias numéricas, no distinguen mayúsculas de minúsculas ni tampoco tienen en cuenta acentos o diacríticos para diferenciar grafemas.
{ numeric: true, sensitivity: 'base' }
En la línea 13, iniciamos un bloque try...catch. Esto nos permite gestionar de forma controlada (líneas 37-41) cualquier error que pueda surgir durante la ordenación, en especial los provocados por los métodos del servicio de hojas de cálculo que estamos utilizando.
En las líneas 15-17 se declara el vector hojasOrdenadas y se inicializa con una lista de objetos que contiene información sobre las hojas de la hoja de cálculo:
- Un objeto de la clase Sheet que identifica a cada hoja proporcionada por el método getSheets.
- El nombre de la propia hoja, obtenido mediante el método getName.
El vector se ordena inmediatamente utilizando el método JS sort encadenado en la línea 18.
☝ El vector de objetos hojasOrdenadas permite economizar llamadas al servicio de hojas de cálculo durante la ordenación, dado que sort invocará internamente la función que compara pares de nombres de hojas tantas veces como lo necesite. Las referencias a cada hoja que hemos guardado en él también resultarán de utilidad en el bucle posterior que actualizará sus posiciones en la lista de hojas.
El resultado numérico devuelto por el objeto Collator (-1, 0, 1) indicará la posición alfabética relativa (anterior, coincidente o posterior, respectivamente) de un par de hojas.
collator.compare(hoja1.nombre, hoja2.nombre)
Lo multiplicaremos por la siguiente expresión matemática, que invertirá su signo cuando se trate de realizar una ordenación descendente.
Math.pow(-1, !ascendente)
¿No lo ves claro 😵💫? Te cuento más cosas sobre cómo funciona internamente la función sort en este artículo previo:
📝 Ordenando alfabéticamente con caracteres propios del idioma en Apps Script
En la línea 21 se identifica la hoja activa, que nos aseguraremos de restaurar como tal tras realizar la ordenación.
A continuación, en las líneas 20-27 es donde movemos las hojas a su posición correcta. Para cada hoja:
- Se comprueba si está oculta y en ese caso se añade al vector hojasPorOcultar.
- Se activa la hoja (esto la hace visible en todo caso).
- Se mueve la hoja a su posición utilizando como referencia su índice (más 1) en el vector hojasOrdenadas.
Finalmente, las líneas 28-35 se encargan de: volver a ocultar las hojas que hemos hecho visibles al ajustar su posición en la lista de hojas, restaurar la hoja activa y mostrar un mensaje informativo.
El uso reiterado del método flush, que fuerza a que se apliquen todos los cambios sobre la hoja de cálculo, así como la pausa de un segundo introducida por medio de la llamada al método sleep, son necesarios, no solo para asegurar la consistencia visual en la interfaz de usuario, sino también para garantizar que la totalidad de las hojas queden ocultas del modo esperado. Te animo a que experimentes con ambas cosas, no obstante.
Lo sé, rarezas de nuestras queridas hojas de cálculo 🤷. Pero las vamos a querer igualmente,¿verdad?
Observa cómo las hojas van cambiando de posición en la barra de pestañas inferior a medida que se van ordenando.

La hoja activa cuando finaliza el proceso (Hoja 4) es la misma que lo estaba inicialmente, aunque ahora aparece en una posición diferente en la barra de hojas como consecuencia de la ordenación alfabética que se ha aplicado.
Por su parte, las hojas 3 y 7 se ordenan junto al resto, pero preservan su estado oculto cuando el script finaliza su ejecución. El proceso completo es razonablemente rápido, unos 4 segundos para 10 hojas.
Todo funciona por tanto según nuestras especificaciones.
Gemini 2.5 Pro, calienta que sales
Mientras estoy preparando este artículo Google ha lanzado una auténtica bomba, su modelo generativo Gemini 2.5 Pro (aún experimental), un modelo que se ha situado rápidamente en lo más alto de todas las tablas comparativas de modelos de lenguaje grandes.
Gemini 2.5 Pro Experimental on Livebench 🤯🥇 pic.twitter.com/532cIY4y00
— Logan Kilpatrick (@OfficialLoganK) March 26, 2025
Así que justo ahora que, coincidiendo casualmente con este lanzamiento 😳, Internet se inunda de todas esas imágenes que pretenden imitar descaradamente el —inimitable— estilo visual de Studio Ghibli, no he podido evitar invitar a Gemini 2.5 Pro a la cocina de este artículo. Sí, este modelo me está gustando mucho.
Lo primero que he hecho ha sido pedirle a Gemini que analice el código que hemos desarrollado en el apartado anterior.

Voy a incluir íntegramente la respuesta del modelo, aunque omitiré su secuencia de pensamiento previa, no porque me parezca que carece de interés, que lo tiene, sino únicamente por brevedad.
💬 Respuesta de Gemini:
¡Excelente! Aquí tienes un análisis del código ordenarHojasServicio:
Análisis General
El script está bien escrito, es funcional y sigue buenas prácticas generales de Apps Script. Su objetivo es ordenar las hojas de cálculo alfabéticamente, manejando tanto el orden ascendente como descendente, y teniendo en cuenta la configuración regional de la hoja de cálculo para una ordenación correcta.
Puntos Fuertes:
1. Funcionalidad Correcta: El script cumple su propósito principal de ordenar las hojas.
2. Manejo de Orden: Utiliza el parámetro ascendente con un valor predeterminado (true) y una lógica (Math.pow(-1, !ascendente)) para invertir el orden de clasificación según sea necesario.
3. Ordenación Internacionalizada (Intl.Collator): El uso de Intl.Collator es excelente. Permite una ordenación alfabética que respeta las reglas del idioma configurado en la hoja de cálculo (obtenido con hdc.getSpreadsheetLocale()), manejando correctamente acentos, mayúsculas/minúsculas (sensitivity: 'base') y potencialmente números dentro de los nombres de hoja (numeric: true). Es mucho más robusto que un simple string.localeCompare() o una comparación directa (<, >).
4. Eficiencia en la Ordenación: La técnica de usar .map() para crear un array de objetos { objeto: hoja, nombre: hoja.getName() } antes de llamar a .sort() es una muy buena práctica. Evita llamar a hoja.getName() (que es una llamada a la API) repetidamente dentro de la función de comparación del sort, lo cual mejora significativamente el rendimiento.
5. Manejo de Hojas Ocultas: El script detecta las hojas que están ocultas antes de moverlas, las mueve (lo que las hace visibles temporalmente) y luego las vuelve a ocultar. Esto preserva el estado de visibilidad de las hojas.
6. Restauración de Hoja Activa: Guarda la hoja activa al inicio (hojaActual) y la restaura al final (hdc.setActiveSheet(hojaActual)). Esto proporciona una buena experiencia de usuario, ya que el usuario no pierde el contexto de en qué hoja estaba trabajando.
7. Uso de SpreadsheetApp.flush(): Se utiliza flush() después de mover las hojas y antes de ocultarlas, y nuevamente al final. Esto ayuda a asegurar que los cambios se apliquen en el backend de Google antes de continuar con el siguiente paso o finalizar el script, lo cual es importante para la consistencia, especialmente con operaciones que afectan la UI o el estado (como la visibilidad de las hojas).
8. Manejo de Errores: Incluye un bloque try...catch que captura errores inesperados y muestra un mensaje útil al usuario a través de ui.alert, incluyendo el mensaje de error específico (e.message), lo cual facilita la depuración.
9. Claridad y Estilo: El código es legible, usa const adecuadamente y tiene nombres de variables descriptivos. Los comentarios (especialmente el JSDoc inicial y el que justifica Utilities.sleep) son útiles.
Posibles Puntos Débiles o Consideraciones:
1. Utilities.sleep(1000): El comentario // Sin esta pausa en ocasiones alguna hoja oculta queda visible indica que este sleep es una solución alternativa (workaround) para un posible problema de sincronización o race condition donde ocultar una hoja inmediatamente después de moverla y hacer flush() no siempre funciona de manera fiable. Si bien es una solución pragmática si el problema se observa consistentemente, introduce un retraso fijo de 1 segundo y hace que el script sea menos eficiente. Idealmente, flush() debería ser suficiente, pero las plataformas a veces tienen estos comportamientos sutiles. No hay una solución obvia si flush() por sí solo no es suficiente, pero es bueno ser consciente de que es un workaround.
2. Rendimiento con Muchas Hojas: El bucle forEach para mover las hojas realiza dos llamadas a la API por cada hoja (setActiveSheet y moveActiveSheet). Si bien la optimización en el sort es excelente, mover las hojas una por una sigue siendo la parte más costosa en términos de tiempo de ejecución. Para hojas de cálculo con una cantidad muy grande de hojas (cientos), el script podría volverse lento o acercarse a los límites de tiempo de ejecución de Apps Script (6 minutos para usuarios normales). Sin embargo, para la mayoría de los casos de uso, este rendimiento es aceptable.
3. Legibilidad de la Lógica de Orden: La expresión Math.pow(-1, !ascendente) funciona perfectamente, pero podría ser ligeramente menos intuitiva para algunos desarrolladores que una simple comprobación ternaria como (ascendente ? 1 : -1). Es una cuestión menor de estilo. ascendente ? collator.compare(...) : collator.compare(...) * -1 sería otra forma.
Conclusión:
Es un script robusto, eficiente (dadas las limitaciones de mover hojas) y bien pensado. Destaca especialmente el uso correcto de Intl.Collator y la optimización para evitar llamadas a getName() dentro del sort. El uso de Utilities.sleep es una solución práctica a un problema potencialmente molesto de la plataforma. En general, es un código de buena calidad para su propósito.
El análisis proporcionado por Gemini me parece excelente. Destaca por su precisión, relevancia y claridad al señalar tanto los puntos fuertes como las posibles debilidades de la implementación.
Por ejemplo, el modelo identifica correctamente que el proceso de reordenación de hojas mediante el servicio Apps Script podría ser ineficiente o problemático al manejar un gran número de ellas (cientos). Aunque abordaremos esta limitación hacia el final del artículo, es notable que Gemini detecte y ponga en evidencia este aspecto de inmediato.
☝ Google suele reservar sus modelos «Pro» para los usuarios de pago de Gemini Advanced. No obstante, puedes experimentar con Gemini 2.5 Pro de manera gratuita pero limitada, en AI Studio.
Y como no podía ser de otro modo, ya lo puedes probar en AI Studio 🆓, aunque con severos límites de uso por minuto. pic.twitter.com/VWsaECeyqA
— Pablo Felip (@pfelipm) March 26, 2025
🔥 Actualización 30/03/25 | Gemini 2.5 Pro (experimental) ya está disponible para todos los usuarios en la aplicación web de Gemini, incluyendo los de cuentas gratuitas y Workspace para Educación, aunque con limites de uso más restrictivos, una ventana de contexto de menor tamaño y, por el momento, sin la función de Canvas.
Gemini, desordena estas pestañas para mí
Visto lo bien que lo ha hecho Gemini 2.5 Pro a la hora de analizar el código, he pensado en ponerlo a prueba una vez más pidiéndole que genere una función similar, pero esta vez para desordenar aleatoriamente las hojas.

Este es el código que me ha proporcionado:
Archivo «Desordenanar (servicio).gs»
/**
* Desordena las hojas de un libro de cálculo de forma aleatoria
* utilizando el servicio integrado de hojas de cálculo.
* Mantiene el estado de visibilidad de las hojas y restaura la hoja activa.
*
* Generado por Gemini 2.5 Pro (experimental) a partir de la función de ordenación ordenarHojasServicio().
*/
function desordenarHojasServicio() {
const hdc = SpreadsheetApp.getActiveSpreadsheet();
const ui = SpreadsheetApp.getUi();
try {
let hojas = hdc.getSheets();
const numHojas = hojas.length;
// Si hay 1 hoja o menos, no hay nada que desordenar
if (numHojas <= 1) {
ui.alert(`Solo hay ${numHojas} hoja(s), no es necesario desordenar.`, ui.ButtonSet.OK);
return;
}
// --- Inicio: Algoritmo de Fisher-Yates (Knuth) Shuffle ---
// Recorre el array desde el final hacia el principio
for (let i = numHojas - 1; i > 0; i--) {
// Elige un índice aleatorio j entre 0 e i (inclusive)
const j = Math.floor(Math.random() * (i + 1));
// Intercambia el elemento en i con el elemento en j
[hojas[i], hojas[j]] = [hojas[j], hojas[i]];
}
// --- Fin: Algoritmo de Fisher-Yates ---
// Ahora el array 'hojas' contiene los objetos Sheet en un orden aleatorio.
const hojasPorOcultar = [];
const hojaActual = hdc.getActiveSheet(); // Guarda la hoja activa actual
// Mueve las hojas a su nueva posición aleatoria
hojas.forEach((hoja, pos) => {
// Comprueba si la hoja estaba oculta ANTES de moverla
// (moverla la hará visible temporalmente si estaba oculta)
if (hoja.isSheetHidden()) {
hojasPorOcultar.push(hoja); // Añade el objeto hoja a la lista para ocultar después
}
hdc.setActiveSheet(hoja); // Activa la hoja para poder moverla
hdc.moveActiveSheet(pos + 1); // Mueve la hoja a su nueva posición (índice + 1)
});
// Necesario confirmar cambios antes de procesar hojas a ocultar
SpreadsheetApp.flush();
// Mantenemos la pausa por consistencia con la función original,
// por si hay problemas de sincronización al ocultar rápidamente.
Utilities.sleep(1000);
// Vuelve a ocultar las hojas que estaban ocultas originalmente
if (hojasPorOcultar.length > 0) {
hojasPorOcultar.forEach(hoja => {
// Podría haber un error si la hoja fue eliminada mientras corría el script,
// aunque es poco probable. Añadimos un check simple.
try {
hoja.hideSheet();
} catch (e) {
console.warn(`No se pudo ocultar la hoja "${hoja.getName()}". ¿Quizás fue eliminada? Error: ${e.message}`);
}
});
}
// Restaura la hoja que estaba activa al principio
hdc.setActiveSheet(hojaActual);
SpreadsheetApp.flush(); // Asegura que la activación final se aplique
ui.alert(`Se han desordenado aleatoriamente ${numHojas} hoja(s).`, ui.ButtonSet.OK);
} catch (e) {
Y estos son los —nuevamente impecables— comentarios con los que Gemini acompaña el código que nos entrega.
💬 Respuesta de Gemini:
Explicación de los Cambios y Similitudes:
1. Nombre y Propósito: La función se llama desordenarHojas y su JSDoc describe claramente su acción.
2. Estructura General: Mantiene la estructura try...catch, la obtención inicial de hdc y ui, y el uso de const para variables.
3. Obtención de Hojas: Se obtienen todas las hojas con hdc.getSheets().
4. Algoritmo de Desorden: En lugar de .map() y .sort(), se implementa el algoritmo Fisher-Yates (Knuth) Shuffle. Este es el método estándar y eficiente para barajar un array aleatoriamente in-place. Modifica directamente el orden de los objetos Sheet dentro del array hojas.
5. Manejo de Hojas Ocultas y Activas: Se sigue exactamente la misma lógica que en ordenarHojasServicio:
- Se guarda la hojaActual.
- Se inicializa hojasPorOcultar.
- Dentro del bucle forEach que mueve las hojas, se comprueba si
hoja.isSheetHidden() antes de moverla y se añade a
hojasPorOcultar si es necesario.
- Se usa setActiveSheet() y moveActiveSheet() para reordenar
físicamente las pestañas según el orden aleatorio del
array hojas.
6. flush() y sleep(): Se mantienen las llamadas a SpreadsheetApp.flush() y Utilities.sleep(1000) en los mismos puntos que en la función original para asegurar la consistencia y prevenir posibles problemas de sincronización al re-ocultar hojas.
7. Re-ocultar Hojas: Se itera sobre hojasPorOcultar y se llama a hideSheet() para cada una. Añadí un pequeño try-catch interno aquí por robustez extra, aunque es poco probable que falle.
8. Restaurar Hoja Activa: Se restaura la hojaActual al final.
9. Mensajes al Usuario: Se usan ui.alert para notificar el éxito o el error, de forma similar a la función original.
10. Caso Básico: Se añade una comprobación al principio por si hay 1 o 0 hojas, en cuyo caso la función termina informando que no hay nada que hacer.
Esta función desordenarHojas complementa a ordenarHojasServicio utilizando las mismas técnicas de manipulación de hojas y manteniendo un estilo de código coherente.
Gemini 2.5 Pro ha respetado el estilo y las estructura de mi función original (incluso ha mantenido el nombre de las variables), ha escogido el clásico pero efectivo algoritmo de Fisher-Yates (Algoritmo P para Knuth) para desordenar las hojas en su versión que reduce el coste espacial al utilizar un solo vector (líneas 23-30) y ha introducido comentarios informativos relevantes en el código.
El resultado ha sido algo que perfectamente podría haber escrito yo mismo, tal cual. Y por supuesto...
☝ El código ha funcionado a la primera. Un «oneshot» de manual para un problema ciertamente no muy complejo, pero resuelto con enorme solvencia por Gemini 2.5 Pro.

Mi única consideración tiene que ver con la declaración del vector hojas en la línea 14 con let en lugar de const, tan solo una cuestión de estilo, cierto:
let hojas = hdc.getSheets();

💬 Respuesta de Gemini:
Tienes toda la razón al preguntar, ¡es una excelente observación!
En este caso específico de la función desordenarHojas, declarar hojas con let no era estrictamente necesario. Podría haberse declarado perfectamente con const y el código funcionaría igual.
// Esta versión también es correcta y quizás preferible:
const hojas = hdc.getSheets();
¿Por qué const funciona aquí?
■ const significa que la asignación a la variable es constante. Es decir, la variable hojas, una vez que se le asigna el array devuelto por hdc.getSheets(), no puede ser reasignada para apuntar a un array diferente o a otro valor.
■ Sin embargo, const NO hace que el valor en sí mismo (en este caso, el array) sea inmutable. Puedes modificar el contenido del array (cambiar el orden de sus elementos, añadir, eliminar elementos) aunque la variable se haya declarado con const.
■ El algoritmo Fisher-Yates que usamos para barajar modifica el array hojas in-place (sobre sí mismo), intercambiando la posición de sus elementos. No reasigna la variable hojas a un nuevo array.
Entonces, ¿por qué usé let en el ejemplo?
Fue más bien por costumbre o por una precaución quizás innecesaria. A veces, al escribir código, uno puede usar let si anticipa que podría necesitar reasignar la variable más adelante, aunque en este flujo particular no ocurra. Sin embargo, la mejor práctica general es:
1. Usar const por defecto.
2. Cambiar a let solo si sabes que necesitarás reasignar la variable.
En resumen: Fue una elección válida pero no obligatoria. Usar const hojas = hdc.getSheets(); sería igualmente correcto y, de hecho, comunica mejor la intención de que la variable hojas siempre se referirá al mismo array (aunque su contenido se modifique). Gracias por señalarlo, ayuda a refinar el código.
Por costumbre, dice 😊... En cualquier caso, tampoco se me ocurre cómo explicarlo mejor.
Ahora, dejemos a Gemini descansar y abordemos la última sección del artículo, el repecho final, de una manera más tradicional.
El poder de la API
Gemini hizo un gran trabajo analizando el código de la nuestra función inicial ordenarHojasServicio. Y señaló muy acertadamente el problema de rendimiento que podría suponer el uso del método moveActiveSheet para mover, una a una, las hojas a su posición de acuerdo con el orden alfabético deseado. Te lo recuerdo:
2. Rendimiento con Muchas Hojas: El bucle forEach para mover las hojas realiza dos llamadas a la API por cada hoja (setActiveSheet y moveActiveSheet). Si bien la optimización en el sort es excelente, mover las hojas una por una sigue siendo la parte más costosa en términos de tiempo de ejecución. Para hojas de cálculo con una cantidad muy grande de hojas (cientos), el script podría volverse lento o acercarse a los límites de tiempo de ejecución de Apps Script (6 minutos para usuarios normales). Sin embargo, para la mayoría de los casos de uso, este rendimiento es aceptable.
¿Tenemos alguna alternativa?
La tenemos. Y además, en este caso es excelente... salvo por un pequeño y poco evidente talón de Aquiles que me reservo para la sección de cierre de este artículo 😏.
Y esta alternativa no es otra que usar la API de Google Sheets.
No es la primera vez que recurrimos a ella en este espacio, la hemos utilizado ya al menos en un par de ocasiones para llegar donde el servicio básico de hojas de cálculo no llega. Por ejemplo, para obtener el mensaje de error que se muestra en una celda que contiene una fórmula defectuosa:
📝 A short story (really!) about errors in Google Sheets formulas, the Sheets API, and being a GDE
Y también para inicializar a la velocidad del rayo todos los controles de filtro (tan útiles, pero al mismo tiempo tan temperamentales) de una hoja de cálculo. Uno de los artículos, por cierto, más populares de mi blog:
📝 Bulk reset slicers in a Google Spreadsheet, the Apps Script way
En lugar de interactuar directamente con la API REST de Sheets por medio de peticiones HTTP mediante el servicio URL Fetch, usaremos en su lugar el servicio avanzado de Sheets, que resulta más amigable. ¡No te olvides de activarlo en el editor Apps Script!

Este servicio avanzado no dispone de tantas funciones como el básico, pero a cambio cuenta con dos ventajas fundamentales:
- Proporciona acceso a todos esos recovecos de una hoja de cálculo que resultan inalcanzables para el servicio básico, como te muestro en el primer artículo referenciado.
- Permite hacer modificaciones en bloque sobre una hoja de cálculo de un modo extremadamente rápido y eficiente por medio de sus solicitudes por lotes.
La implementación de esta nueva función ordenarHojasApi resulta sorprendentemente compacta:
Archivo «Ordenar (API).gs»
/**
* Ordena las hojas de un libro de cálculo alfabéticamente en sentido ascendente o descendente
* utilizando el servicio avanzado (API) de hojas de cálculo.
* @param {boolean} [ascendente]
*/
function ordenarHojasApi(ascendente = true) {
const hdc = SpreadsheetApp.getActiveSpreadsheet();
const ui = SpreadsheetApp.getUi();
// Precisamos únicamente las letras correspondientes al código del idioma
const collator = new Intl.Collator(hdc.getSpreadsheetLocale().split('_')[0], { numeric: true, sensitivity: 'base' });
try {
// Obtiene vector de propiedades de hojas (nombre, índice)
const hojasOrdenadas = Sheets.Spreadsheets.get(
SpreadsheetApp.getActiveSpreadsheet().getId(), { fields: 'sheets.properties(sheetId,title)' }
).sheets.sort((hoja1, hoja2) =>
Math.pow(-1, !ascendente) * collator.compare(hoja1.properties.title, hoja2.properties.title)
);
const hojaActual = hdc.getActiveSheet();
Sheets.Spreadsheets.batchUpdate(
{
requests: hojasOrdenadas.map(({ properties: { sheetId } }, pos) => (
{ updateSheetProperties: { properties: { sheetId, index: pos }, fields: 'index' } }
)),
includeSpreadsheetInResponse: false
},
hdc.getId()
);
hdc.setActiveSheet(hojaActual);
ui.alert(`Se han ordenado alfabéticamente ${hojasOrdenadas.length} hoja(s) en sentido ${ascendente ? 'ascendente' : 'descendente'}.`, ui.ButtonSet.OK);
} catch (e) {
ui.alert(`Se ha producido un error inesperado al ordenar las hojas, inténtalo de nuevo.
⚠️ ${e.message}`, ui.ButtonSet.OK);
}
}
El código que te presento, totalmente artesanal (no se ha utilizando ningún modelo generativo), resulta idéntico al de la función ordenarHojasServicio hasta el bloque try...catch que inicia en la línea 13. Aquí comienzan las diferencias.
En la línea 17 se obtiene la información imprescindible de la colección de hojas por medio de una única llamada al método Spreadsheets.get del servicio avanzado. Precisamos conocer exclusivamente los campos que contienen el identificador (sheetId) y nombre (title) de cada hoja, por lo que se utiliza una máscara de campos acorde. Al igual que en nuestra primera versión de la función de ordenación, la posición actual de cada hoja nos resulta indiferente. De este modo evitaremos que la API transmita información innecesaria, mejorando el rendimiento de la operación.
{ fields: 'sheets.properties(sheetId,title)' }
Como puedes ver, el uso de máscaras de campos constituye otra característica clave de la API de Sheets. Es importante destacar que también dispondremos de esta funcionalidad cuando consumamos la API a través de su servicio avanzado Apps Script, que actúa como una capa de abstracción intermedia o envoltorio ligero para la API subyacente.
Las hojas se ordenan a continuación en las líneas 18 y 19 del modo habitual mediante el método sort y su collator convenientemente configurado, y justo después, en la línea 22, se guarda la hoja activa, que será restaurada al finalizar.
Aquí viene lo bueno, gracias al poder de la API 💪:
- No tendremos que preocuparnos de gestionar las hojas ocultas, cuya posición en la lista de hojas estableceremos sin necesidad de hacerlas previamente visibles, ni por consiguiente ocultarlas tras la ordenación.
- Actualizaremos de una vez las posiciones de todas la hojas mediante una única llamada al método spreadsheets.batchUpdate.
De todo esto se encargan las líneas 23-31, donde se construye un vector de peticiones con la estructura necesaria, que se inserta dentro del cuerpo de la solicitud (request body) del método anteriormente indicado.

El resto es lo de siempre: restaurar la hoja activa (línea 32), informar al usuario (línea 33) y, en su caso, tratar las excepciones que se pudieran haber producido en tiempo de ejecución (líneas 34-39).
Mi experiencia con esta implementación es que, por su diseño, evita los problemas de consistencia descritos que afectan al servicio básico de hojas de cálculo, por lo que el uso de los métodos flush o sleep no parece ser necesario.
Te estarás preguntando si la diferencia es realmente tan notable. Juzga por ti mismo.

Como puedes apreciar, el proceso de ordenación es ahora fulminante ⚡. Sí, la diferencia es más que notable.
Me gustaría destacar que cada solicitud por lotes (batchUpdate) a la API de Sheets cuenta como una única operación para los límites del servicio, lo cual es una ventaja muy significativa.
Aunque la documentación oficial no especifica un número máximo de operaciones por lote, existen límites prácticos basados en el tamaño total de la solicitud y el tiempo de ejecución. Sin embargo, dado que una hoja de cálculo con cientos de pestañas probablemente no sea la cosa más manejable del mundo, es poco probable que estos límites prácticos representen un problema en nuestro caso de uso particular.
☝ Mis pruebas indican que el rendimiento es adecuado, tanto con el servicio básico como con la API, cuando ordenamos unas pocas decenas de pestañas. Sin embargo, a partir de aproximadamente 80 hojas, el rendimiento se degrada considerablemente, al parecer debido a la gestión interna de pestañas de Google Sheets. En este punto, las diferencias de rendimiento entre ambos métodos se reducen de forma significativa. Esta parece ser una de esas madrigueras de conejo en las que perderse durante horas, así que por hoy lo vamos a dejar ahí.
Dicho esto, para asegurar la robustez de tus scripts, es fundamental que te mantengas siempre informado sobre los límites vigentes tanto de la API de Sheets como de Apps Script.
¿Le podríamos haber pedido a Gemini 2.5 Pro que diseñase esta nueva implementación basada en el uso del servicio avanzado de hojas de cálculo?
Por supuesto.
¿Y lo hubiera hecho bien? Aunque no te voy a mostrar el código escrito por Gemini, perfectamente 👌, seleccionando la estrategia correcta (batchupdate) con la máscara de campos apropiada, idéntica a la que he utilizado yo.
El único minúsculo "pero" que podría poner encima de la mesa es que en la versión generada por Gemini no se ajusta de manera explícita el parámetro includeSpreadsheetInResponse a false en la petición por lotes para evitar que la llamada a la API nos devuelva el recurso completo de la hoja de cálculo, que no necesitamos. Pero es que no es estrictamente necesario hacerlo, y Gemini lo tiene muy claro e incluso lo justifica perfectamente, referenciando correctamente la documentación oficial.

Te propongo, a modo de ejercicio práctico, que implementes una función para desordenar las hojas utilizando la API. Con Gemini o sin él 😉.
Entonces... ¿servicio integrado o API?
A lo largo de este artículo te he mostrado cómo ordenar las pestañas de una hoja de cálculo utilizando tanto el servicio integrado en Apps Script como la API de hojas de cálculo, a través de su correspondiente servicio avanzado.
Hemos podido constatar que la segunda opción (API) es claramente la más conveniente en términos de rendimiento. ¿La usaremos entonces siempre? Depende.
☝ El origen de este dilema hay que buscarlo en los permisos o ámbitos de autorización (authorization scopes, en su denominación original) necesarios para que un script GAS como los que hemos desarrollado a lo largo de este artículo funcione correctamente.
Las funciones ordenarHojasServicio y desordenarHojasServicio precisan únicamente la autorización necesaria para modificar la hoja de cálculo sobre la que se ejecutan:
https://www.googleapis.com/auth/spreadsheets.currentonly
Esto se corresponde con el ámbito de autorización mostrado, que cuando se confía en la autodetección de scopes característica de Apps Script puede forzarse de manera inmediata insertando la conocida anotación @OnlyCurrentDoc en los comentarios JSDoc del código.
En cambio, el método Spreadsheets.batchUpdate en el que se basa la función ordenarHojasApi requiere uno de los siguientes ámbitos de autorización:
https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/drive.file
https://www.googleapis.com/auth/spreadsheets
Pero resulta que:
- /drive nos da acceso de lectura y escritura no solo a las hojas de cálculo sino a todos los archivos almacenados en Drive del usuario (excesivo).
- /drive.file solo concede permiso para acceder a los archivos que el script haya creado o que el usuario haya abierto explícitamente mientras lo usa, pero no a algo tan simple como la hoja de cálculo activa (no aplica).
- /spreadsheets concede acceso a cualquier hoja de cálculo, no solo a la activa (también excesivo, aunque en menor medida).
Que un script GAS solicite permisos más amplios de los estrictamente necesarios para funcionar se considera una mala práctica, que está directamente prohibida cuando se trata de publicarlo en la tienda oficial de aplicaciones de Google Workspace.
Por ejemplo, mi complemento para Google Sheets HdC+, que integra entre muchas otras herramientas de gestión de hojas la posibilidad de ordenarlas, está diseñado para funcionar con los permisos estrictamente necesarios:

Como puedes suponer, la implementación de la función de ordenación de hojas de HdC+ está basada en el servicio integrado Apps Script de hojas de cálculo, en lugar de en la más potente API de Google Sheets, que actualmente no ofrece un scope equivalente a currentonly para este tipo de operaciones.
Así que aquí me tienes, sintiendo en el aire la tensión entre la eficiencia / rendimiento y el principio de privilegio mínimo en los ámbitos de autorización. Pero por ahora, mantener este enfoque alineado con el uso de los permisos más estrictos va a seguir siendo mi prioridad 😒.
☝ En un próximo artículo vamos a diseñar una barra de progreso, fácilmente reutilizable, para representar de manera visual el avance de la ordenación y mejorar así la experiencia de usuario en la versión "lenta" del script, que utiliza el servicio integrado de hojas de cálculo.
Conclusiones y siguientes pasos
Y como siempre, llegamos al final. O este llega a nosotros, quién sabe.
Invitar a un modelo «pensante» tan potente como Gemini 2.5 Pro como pinche en este artículo y comprobar lo bien que se ha comportado —tan fascinantes me han parecido sus pensamientos como las respuesta finales— me ha dejado profundamente pensativo.
Debo confesar que esta es la primera vez que he disfrutado genuinamente trabajando junto a un modelo generativo en una de mis publicaciones. Gemini me ha amplificado discretamente, sin resultar un estorbo ni interferir, y ha sido mi editor técnico y corrector de estilo. Creo honestamente que con su asistencia he alcanzado un resultado final superior al que hubiera sido capaz de lograr trabajando en solitario durante el mismo periodo de tiempo.
Sus explicaciones y comentarios sobre las distintas implementaciones desarrolladas no han sido tan solo correctos, sino tan detallados y útiles que constituirían un valioso recurso para cualquier persona que necesitara comprender a fondo el funcionamiento de este código. Quizás en ocasiones tanto o más que mis propias palabras 🙄.
Y claro, eso me conduce a la duda ineludible...
🤔 ¿De qué modo puedo seguir aportando valor de manera significativa con mi trabajo en este contexto actual de adopción acelerada de la IA generativa?
¿Debería continuar invirtiendo un tiempo considerable en desglosar cada detalle, en desmenuzar cada bloque de código, cada fórmula, cada decisión de diseño en mis artículos?
¿Bastaría con pulir los comentarios de una IA potente como Gemini, aunque eso hiciera mis textos menos personales, menos centrados en lo que yo considero clave? Menos humanos, en definitiva.
¿Tiene sentido mantener el foco en el cómo (el código) o hay que ponerlo ahora inevitablemente en el qué (el producto)?
La verdad es que no tengo respuestas definitivas, pero sí la certeza de que este va a ser el dilema a discutir conmigo mismo la próxima vez que me calce la zapatillas y salga a correr.
Y, ¿tú cómo lo ves? Me gustaría mucho leer tu opinión en los comentarios.
Comentarios