web-dev-qa-db-fra.com

Un moyen plus rapide de trouver la première ligne vide

J'ai créé un script qui ajoute toutes les quelques heures une nouvelle ligne à une feuille de calcul Google Apps.

Voici la fonction que j'ai créée pour trouver la première ligne vide:

function getFirstEmptyRow() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var cell = spr.getRange('a1');
  var ct = 0;
  while ( cell.offset(ct, 0).getValue() != "" ) {
    ct++;
  }
  return (ct);
}

Cela fonctionne bien, mais lorsque vous atteignez environ 100 lignes, cela devient vraiment lent, même dix secondes… .. Je crains que lorsque vous atteignez des milliers de lignes, ce soit trop lent, peut-être avec un dépassement de délai ou pire… Y a-t-il un meilleur moyen?

29
Omiod

Le blog Google Apps Script contenait un article sur optimisant les opérations de tableur qui parlait de la mise en lots de lectures et de lectures en écriture, qui pouvait réellement accélérer les choses. J'ai essayé votre code sur une feuille de calcul avec 100 lignes et cela a pris environ sept secondes. En utilisant Range.getValues() , la version par lot prend une seconde.

function getFirstEmptyRow() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var column = spr.getRange('A:A');
  var values = column.getValues(); // get all data in one call
  var ct = 0;
  while ( values[ct][0] != "" ) {
    ct++;
  }
  return (ct);
}

Si la feuille de calcul devient suffisamment grande, vous devrez peut-être récupérer les données sous forme de fragments de 100 ou 1000 lignes au lieu de récupérer la totalité de la colonne.

42
Don Kirkby

Cette question a maintenant eu plus de 12K vues - il est donc temps d'effectuer une mise à jour, car les performances de New Sheets sont différentes de celles de quand { Serge a effectué ses premiers tests }.

Bonne nouvelle: les performances sont bien meilleures dans tous les domaines!

Le plus rapide:

Comme lors du premier test, la lecture des données de la feuille une seule fois, puis son exploitation sur la matrice, ont permis d'améliorer considérablement les performances. Il est intéressant de noter que la fonction initiale de Don fonctionnait bien mieux que la version modifiée testée par Serge. (Il semble que while est plus rapide que for, ce qui n’est pas logique.)

Le temps d'exécution moyen sur les données d'échantillon est juste de 38ms, en baisse par rapport au précédent 168ms.

// Don's array approach - checks first column only
// With added stopping condition & correct result.
// From answer https://stackoverflow.com/a/9102463/1677912
function getFirstEmptyRowByColumnArray() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var column = spr.getRange('A:A');
  var values = column.getValues(); // get all data in one call
  var ct = 0;
  while ( values[ct] && values[ct][0] != "" ) {
    ct++;
  }
  return (ct+1);
}

Résultats de test:

Voici les résultats, résumés sur 50 itérations dans un tableur de 100 lignes x 3 colonnes (rempli avec la fonction de test de Serge).

Les noms de fonction correspondent au code dans le script ci-dessous. 

screenshot

"Première rangée vide"

La demande initiale était de trouver la première ligne vide. Aucun des scripts précédents ne répond réellement à cela. Beaucoup vérifient une seule colonne, ce qui signifie qu'ils peuvent donner des résultats faux positifs. D'autres ne trouvent que la première ligne qui suit toutes les données, ce qui signifie que les lignes vides dans des données non contiguës sont manquantes.

Voici une fonction qui répond aux spécifications. Il était inclus dans les tests et, bien que plus lent que le vérificateur ultrarapide à une colonne, il atteignait un temps respectable de 68 ms, une prime de 50% pour une réponse correcte!

/**
 * Mogsdad's "whole row" checker.
 */
function getFirstEmptyRowWholeRow() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var range = sheet.getDataRange();
  var values = range.getValues();
  var row = 0;
  for (var row=0; row<values.length; row++) {
    if (!values[row].join("")) break;
  }
  return (row+1);
}

Script complet:

Si vous souhaitez répéter les tests ou ajouter votre propre fonction au mixage à titre de comparaison, prenez tout le script et utilisez-le dans un tableur.

/**
 * Set up a menu option for ease of use.
 */
function onOpen() {
  var menuEntries = [ {name: "Fill sheet", functionName: "fillSheet"},
                      {name: "test getFirstEmptyRow", functionName: "testTime"}
                     ];
  var sh = SpreadsheetApp.getActiveSpreadsheet();
  sh.addMenu("run tests",menuEntries);
}

/**
 * Test an array of functions, timing execution of each over multiple iterations.
 * Produce stats from the collected data, and present in a "Results" sheet.
 */
function testTime() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  ss.getSheets()[0].activate();
  var iterations = parseInt(Browser.inputBox("Enter # of iterations, min 2:")) || 2;

  var functions = ["getFirstEmptyRowByOffset", "getFirstEmptyRowByColumnArray", "getFirstEmptyRowByCell","getFirstEmptyRowUsingArray", "getFirstEmptyRowWholeRow"]

  var results = [["Iteration"].concat(functions)];
  for (var i=1; i<=iterations; i++) {
    var row = [i];
    for (var fn=0; fn<functions.length; fn++) {
      var starttime = new Date().getTime();
      eval(functions[fn]+"()");
      var endtime = new Date().getTime();
      row.Push(endtime-starttime);
    }
    results.Push(row);
  }

  Browser.msgBox('Test complete - see Results sheet');
  var resultSheet = SpreadsheetApp.getActive().getSheetByName("Results");
  if (!resultSheet) {
    resultSheet = SpreadsheetApp.getActive().insertSheet("Results");
  }
  else {
    resultSheet.activate();
    resultSheet.clearContents();
  }
  resultSheet.getRange(1, 1, results.length, results[0].length).setValues(results);

  // Add statistical calculations
  var row = results.length+1;
  var rangeA1 = "B2:B"+results.length;
  resultSheet.getRange(row, 1, 3, 1).setValues([["Avg"],["Stddev"],["Trimmed\nMean"]]);
  var formulas = resultSheet.getRange(row, 2, 3, 1);
  formulas.setFormulas(
    [[ "=AVERAGE("+rangeA1+")" ],
     [ "=STDEV("+rangeA1+")" ],
     [ "=AVERAGEIFS("+rangeA1+","+rangeA1+',"<"&B$'+row+"+3*B$"+(row+1)+","+rangeA1+',">"&B$'+row+"-3*B$"+(row+1)+")" ]]);
  formulas.setNumberFormat("##########.");

  for (var col=3; col<=results[0].length;col++) {
    formulas.copyTo(resultSheet.getRange(row, col))
  }

  // Format for readability
  for (var col=1;col<=results[0].length;col++) {
    resultSheet.autoResizeColumn(col)
  }
}

// Omiod's original function.  Checks first column only
// Modified to give correct result.
// question https://stackoverflow.com/questions/6882104
function getFirstEmptyRowByOffset() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var cell = spr.getRange('a1');
  var ct = 0;
  while ( cell.offset(ct, 0).getValue() != "" ) {
    ct++;
  }
  return (ct+1);
}

// Don's array approach - checks first column only.
// With added stopping condition & correct result.
// From answer https://stackoverflow.com/a/9102463/1677912
function getFirstEmptyRowByColumnArray() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var column = spr.getRange('A:A');
  var values = column.getValues(); // get all data in one call
  var ct = 0;
  while ( values[ct] && values[ct][0] != "" ) {
    ct++;
  }
  return (ct+1);
}

// Serge's getFirstEmptyRow, adapted from Omiod's, but
// using getCell instead of offset. Checks first column only.
// Modified to give correct result.
// From answer https://stackoverflow.com/a/18319032/1677912
function getFirstEmptyRowByCell() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var ran = spr.getRange('A:A');
  var arr = []; 
  for (var i=1; i<=ran.getLastRow(); i++){
    if(!ran.getCell(i,1).getValue()){
      break;
    }
  }
  return i;
}

// Serges's adaptation of Don's array answer.  Checks first column only.
// Modified to give correct result.
// From answer https://stackoverflow.com/a/18319032/1677912
function getFirstEmptyRowUsingArray() {
  var sh = SpreadsheetApp.getActiveSpreadsheet();
  var ss = sh.getActiveSheet();
  var data = ss.getDataRange().getValues();
  for(var n=0; n<data.length ;  n++){
    if(data[n][0]==''){n++;break}
  }
  return n+1;
}

/**
 * Mogsdad's "whole row" checker.
 */
function getFirstEmptyRowWholeRow() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var range = sheet.getDataRange();
  var values = range.getValues();
  var row = 0;
  for (var row=0; row<values.length; row++) {
    if (!values[row].join("")) break;
  }
  return (row+1);
}

function fillSheet(){
  var sh = SpreadsheetApp.getActiveSpreadsheet();
  var ss = sh.getActiveSheet();
  for(var r=1;r<1000;++r){
    ss.appendRow(['filling values',r,'not important']);
  }
}

// Function to test the value returned by each contender.
// Use fillSheet() first, then blank out random rows and
// compare results in debugger.
function compareResults() {
  var a = getFirstEmptyRowByOffset(),
      b = getFirstEmptyRowByColumnArray(),
      c = getFirstEmptyRowByCell(),
      d = getFirstEmptyRowUsingArray(),
      e = getFirstEmptyRowWholeRow(),
      f = getFirstEmptyRowWholeRow2();
  debugger;
}
34
Mogsdad

C'est déjà là comme méthode getLastRow sur la feuille.

var firstEmptyRow = SpreadsheetApp.getActiveSpreadsheet().getLastRow() + 1;

Réf https://developers.google.com/apps-script/class_sheet#getLastRow

21
Peter Herrmann

En voyant cet ancien message avec 5k vues, j'ai d'abord vérifié le 'meilleure réponse' et j'ai été assez surpris par son contenu ... c'était vraiment un processus très lent! alors je me sentais mieux quand j'ai vu la réponse de Don Kirkby, l'approche par tableaux est en effet beaucoup plus efficace!

Mais combien plus efficace?

Alors j'ai écrit ce petit code de test sur une feuille de calcul avec 1000 lignes et voici les résultats: (pas mal! ... pas besoin de dire lequel est lequel ...)

enter image description hereenter image description here

et voici le code que j'ai utilisé:

function onOpen() {
  var menuEntries = [ {name: "test method 1", functionName: "getFirstEmptyRow"},
                      {name: "test method 2 (array)", functionName: "getFirstEmptyRowUsingArray"}
                     ];
  var sh = SpreadsheetApp.getActiveSpreadsheet();
  sh.addMenu("run tests",menuEntries);
}

function getFirstEmptyRow() {
  var time = new Date().getTime();
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var ran = spr.getRange('A:A');
  for (var i= ran.getLastRow(); i>0; i--){
    if(ran.getCell(i,1).getValue()){
      break;
    }
  }
  Browser.msgBox('lastRow = '+Number(i+1)+'  duration = '+Number(new Date().getTime()-time)+' mS');
}

function getFirstEmptyRowUsingArray() {
  var time = new Date().getTime();
  var sh = SpreadsheetApp.getActiveSpreadsheet();
  var ss = sh.getActiveSheet();
  var data = ss.getDataRange().getValues();
  for(var n =data.length ; n<0 ;  n--){
    if(data[n][0]!=''){n++;break}
  }
  Browser.msgBox('lastRow = '+n+'  duration = '+Number(new Date().getTime()-time)+' mS');
}

function fillSheet(){
  var sh = SpreadsheetApp.getActiveSpreadsheet();
  var ss = sh.getActiveSheet();
  for(var r=1;r<1000;++r){
    ss.appendRow(['filling values',r,'not important']);
  }
}

Et le tableur de test à essayer vous-même :-)


MODIFIER :

Après le commentaire de Mogsdad, je devrais mentionner que ces noms de fonctions sont en effet un mauvais choix ... Cela aurait dû être quelque chose comme getLastNonEmptyCellInColumnAWithPlentyOfSpaceBelow() qui n’est pas très élégant (est-ce?) Mais plus précis et cohérent avec ce qu’il retourne réellement.

Commentaire: 

Quoi qu'il en soit, mon but était de montrer la rapidité d'exécution des deux approches, et il l'a évidemment fait (n'est-ce pas? ;-)

8
Serge insas

Je sais que c'est un vieux fil et il y a eu des approches très intelligentes ici.

J'utilise le script

var firstEmptyRow = SpreadsheetApp.getActiveSpreadsheet().getLastRow() + 1;

si j'ai besoin de la première ligne complètement vide.

Si j'ai besoin de la première cellule vide d'une colonne, je procède comme suit.

  • Ma première ligne est généralement une ligne de titre.
  • Ma deuxième rangée est une rangée cachée et chaque cellule a la formule

    =COUNTA(A3:A)
    

    A est remplacé par la lettre de la colonne.

  • Mon script lit simplement cette valeur. Cela met à jour assez rapidement par rapport aux approches de script.

Il arrive une fois que cela ne fonctionne pas et que je permets à des cellules vides de décomposer la colonne. Je n'ai pas encore eu besoin d'un correctif pour cela, je suppose qu'une peut être dérivée de COUNTIF, ou d'une fonction combinée ou de l'une des nombreuses autres fonctions intégrées.

EDIT:COUNTA gère les cellules vides dans une plage, de sorte que l'inquiétude suscitée par "une fois que cela ne fonctionne pas" n'est pas vraiment une préoccupation. (Cela pourrait être un nouveau comportement avec "nouvelles feuilles".)

4
Niccolo

Et pourquoi ne pas utiliser appendRow ?

var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
spreadsheet.appendRow(['this is in column A', 'column B']);
3
Sonny

En effet, getValues ​​est une bonne option mais vous pouvez utiliser la fonction .length pour obtenir la dernière ligne.

 function getFirstEmptyRow() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var array = spr.getDataRange().getValues();
  ct = array.length + 1
  return (ct);
}
2
Thomas

J'ai un problème similaire. À l’heure actuelle, il s’agit d’un tableau comportant plusieurs centaines de lignes et je pense qu’il atteindra plusieurs milliers. (Je n'ai pas vu si une feuille de calcul Google traiterait des dizaines de milliers de lignes, mais j'y parviendrai éventuellement.)

Voici ce que je fais.

  1. Parcourez la colonne par centaines, arrêtez-vous lorsque je suis sur une ligne vide.
  2. Revenez en arrière dans la colonne par dizaines, en recherchant la première ligne non vide.
  3. Parcourez les colonnes en avant, en recherchant la première ligne vide.
  4. Renvoie le résultat.

Cela dépend bien sûr du contenu contigu. Vous ne pouvez pas avoir de lignes vides aléatoires ici. Ou du moins, si vous le faites, les résultats seront sous-optimaux. Et vous pouvez ajuster les incréments si vous pensez que c'est important. Celles-ci fonctionnent pour moi et je trouve que la différence de durée entre les pas de 50 et les pas de 100 est négligeable.

function lastValueRow() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var r = ss.getRange('A1:A');
  // Step forwards by hundreds
  for (var i = 0; r.getCell(i,1).getValue() > 1; i += 100) { }
  // Step backwards by tens
  for ( ; r.getCell(i,1).getValue() > 1; i -= 10) { }
  // Step forwards by ones
  for ( ; r.getCell(i,1).getValue() == 0; i--) { }
  return i;
}

C'est beaucoup plus rapide que d'inspecter chaque cellule par le haut. Et si vous avez d'autres colonnes qui étendent votre feuille de calcul, cela peut être plus rapide que d'inspecter chaque cellule à partir du bas.

1
ghoti

Je conserve une feuille supplémentaire de "maintenance" sur mes feuilles de calcul où je conserve ces données.

Pour obtenir la prochaine ligne libre d'une plage, il suffit d'examiner la cellule appropriée. Je peux obtenir la valeur instantanément, car le travail de recherche de la valeur se produit lorsque les données sont modifiées.

La formule dans la cellule est généralement quelque chose comme:

=QUERY(someSheet!A10:H5010, 
    "select min(A) where A > " & A9 & " and B is null and D is null and H < 1")

La valeur dans A9 peut être définie périodiquement sur une ligne proche de "assez" jusqu'à la fin.

Avertissement : Je n'ai jamais vérifié si cela était viable pour des ensembles de données volumineux. 

0
Martin Bramwell

L'utilisation de indexOf est l'un des moyens d'y parvenir:

 function firstEmptyRow () {
 var ss = SpreadsheetApp.getActiveSpreadsheet (); 
 var sh = ss.getActiveSheet (); 
 var rangevalues ​​= sh.getRange (1,1, sh.getLastRow (), 1) .getValues ​​(); // Colonne A: A est pris 
 var dat = rangevalues.reduce (fonction (a, b) {retour a.concat (b)}, []); // 
 Le tableau 2D est réduit à 1D //
 // Array.prototype.Push.apply est peut-être plus rapide, mais ne parvient pas à le faire fonctionner //
 var fner = 1 + dat.indexOf (''); // extrait indexOf Première ligne vide 
 return (fner); 
 } 
0
TheMaster

Enfin, j'ai une solution en une seule ligne pour cela.

var sheet = SpreadsheetApp.getActiveSpreadsheet();
var lastEmptyOnColumnB = sheet.getRange("B1:B"+sheet.getLastRow()).getValues().join(",").replace(/,,/g, '').split(",").length;

Ça fonctionne bien pour moi.

0
Hari Das

Juste mes deux centimes, mais je le fais tout le temps. Je viens d'écrire les données en haut de la feuille. La date est inversée (dernière en haut), mais je peux quand même le faire faire ce que je veux. Le code ci-dessous stocke les données qu’il récupère du site d’un agent immobilier au cours des trois dernières années.

var theSheet = SpreadsheetApp.openById(zSheetId).getSheetByName('Sheet1');
theSheet.insertRowBefore(1).getRange("A2:L2").setValues( [ zPriceData ] );

Cette partie de la fonction racleur insère une ligne au-dessus de n ° 2 et y écrit les données. La première ligne est l'en-tête, donc je ne touche pas à cela. Je ne l'ai pas chronométré, mais la seule fois où j'ai un problème, c'est lorsque le site change.

0
HardScale

J'ai modifié le code fourni par ghoti afin qu'il recherche une cellule vide. La comparaison de valeurs ne fonctionnait pas sur une colonne avec du texte (ou je ne pouvais pas comprendre comment) mais j'ai utilisé isBlank () Notez que la valeur est inversée! (devant la variable r) lorsqu’on regarde en avant puisque vous voulez que j’augmente jusqu’à ce qu’un blanc soit trouvé. En travaillant sur la feuille par dix, vous souhaitez arrêter de diminuer lorsque vous trouvez une cellule qui n'est pas vide (! Supprimé). Ensuite, redescendez d’un à un le papier vierge.

function findRow_() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  ss.setActiveSheet(ss.getSheetByName("DAT Tracking"));
  var r = ss.getRange('C:C');
  // Step forwards by hundreds
  for (var i = 2; !r.getCell(i,1).isBlank(); i += 100) { }
  // Step backwards by tens
  for ( ; r.getCell(i,1).isBlank(); i -= 10) { }
  // Step forwards by ones
  for ( ; !r.getCell(i,1).isBlank(); i++) { }
  return i;
0
Richard Rasch