0

I have a workbook with multiple sheets, and the main sheet has 123 rows and 90 columns currently.

I've coded the following functions (which work) to define an onChange event handler for the INSERT_COLUMN event that automatically populates the cells of the newly-inserted column with the contents of the column immediately to the left. Then it deletes the values of the cells that are not formulas.

It's painfully slow, and I'm not sure why.

How can I speed this up? Thanks.

function getColumnLetter(a1Notation) {
  const letter = a1Notation.replace(/\d+/, ''); 
  return letter;
}

function getColumnLetterFromNumber(sheet, colNum) {
  const range = sheet.getRange(1, colNum); 
  return getColumnLetter(range.getA1Notation());
}

function forEachRangeCell(range, callback) {
  const numRows = range.getNumRows();
  const numCols = range.getNumColumns();

  for (let i = 1; i <= numCols; i+=1) {
    for (let j = 1; j <= numRows; j+=1) {
      const cell = range.getCell(j, i);

      callback(cell);
    }
  }
}

function deleteAllValuesAndNotesFromNonFormulaCells(range) {
  forEachRangeCell(range, function (cell) {
    if(!cell.getFormula()){ 
      cell.setValue(null);
      cell.clearNote();
    }
  });
}

function onInsertColumn(sheet, activeRng) {  
  if (activeRng.isBlank()) {
    const minCol = 5;
    const col = activeRng.getColumn();
    if (col >= minCol) {
      const prevCol = col - 1;    
      const colLetter = getColumnLetterFromNumber(sheet, col);    
      const prevColLetter = getColumnLetterFromNumber(sheet, prevCol);
      
      //SpreadsheetApp.getUi().alert(`Please wait while formulas are copied to the new column...`);
      const originRng = sheet.getRange(`${prevColLetter}:${prevColLetter}`);    
      originRng.copyTo(activeRng, SpreadsheetApp.CopyPasteType.PASTE_NORMAL, false);    
      deleteAllValuesAndNotesFromNonFormulaCells(activeRng);
      const completeMsg = `New column ${colLetter} has formulas copied and is ready for new values (such as address, Redfin link, data, ratings).`;
      //SpreadsheetApp.getUi().alert(completeMsg);
      // SpreadsheetApp.getActiveSpreadsheet().toast(completeMsg);
    }
  }
}

function onChange(event) {   
  if(event.changeType === 'INSERT_COLUMN'){
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getActiveSheet()
    const colNumber = sheet.getSelection().getActiveRange().getColumn(); 

    const activeRng = sheet.getRange(1,colNumber,sheet.getMaxRows(),1);

    const sheetName = sheet.getName();
  
    if(sheetName === 'ratings'){
      onInsertColumn(sheet, activeRng);
    }
  }
}
Ryan
  • 22,332
  • 31
  • 176
  • 357
  • @MikeSteelson Lots of different types of formulas. Here are examples: `=if(or(isblank(K8),ISBLANK(K15)),"",K8/K15)`, `=if(isblank(K$3),"",if(isblank(K21),ROUTE_MATRIX(K$3, $E$24:$E$31),K21))`, `=if(ISBLANK(K$21),"",DRIVE_TIME(K$21,$C24))`, `=if(isblank(K4),0,IF(COUNTIF($AL$122:$122,K122)>1,1,0))`. Some use custom functions like ROUTE_MATRIX or DRIVE_TIME, but I figured they shouldn't be slow because of the `IF` part short-circuiting them. Thanks. – Ryan Jun 11 '22 at 14:25
  • How do you know if the INSERT_COLUMN was to the left or to the right? That affects your `activeRng `. – TheWizEd Jun 11 '22 at 14:33
  • It's "painfully slow" because the use of Google Apps Script methods in nested for loops. See https://developers.google.com/apps-script/guides/support/best-practices#use_batch_operations – Rubén Jun 11 '22 at 14:58

2 Answers2

2

Not sure if I fully understand the problem. So here is a guess.

I'd try to change the function with the fancy name deleteAllValuesAndNotesFromNonFormulaCells() this way:

function deleteAllValuesAndNotesFromNonFormulaCells(range) {

  // get the array with all formulas
  var formulas = range.getFormulas();

  // set all formulas back (it will clear all cells with no formula) 
  range.setFormulas(formulas);

  // get the array with all notes and
  // clear the ements of the 'notes' array that are empty in the array 'formulas'
  var notes = range.getNotes().map((x,i) => formulas[i][0] ? x : ['']);

  // set the modified array 'notes' back on the sheet
  range.setNotes(notes);
}

If you don't need to keep the notes the function can be boiled down to just one line:

function deleteAllValuesAndNotesFromNonFormulaCells(range) {
  range.setFormulas(range.getFormulas()).clearNote();
}
Yuri Khristich
  • 13,448
  • 2
  • 8
  • 23
  • +1 because I think it's giving me an idea: https://stackoverflow.com/questions/72584045/how-to-speed-up-surprisingly-slow-google-sheets-apps-script-functions-that-copy#comment128222118_72586781 – Ryan Jun 11 '22 at 19:26
  • What about notes? The code from accepted answer removes all notes from the destination range. But your original code keeps the copied notes. – Yuri Khristich Jun 11 '22 at 19:58
  • My original code clears notes too in `deleteAllValuesAndNotesFromNonFormulaCells` via `cell.clearNote();`. – Ryan Jun 11 '22 at 20:56
  • 1
    It removes a note only if the cell has no formula `if(!cell.getFormula()){...` – Yuri Khristich Jun 11 '22 at 22:23
  • You are right! ‍♂️ Thank you. I need to add some of your code too to mine. Thanks. – Ryan Jun 11 '22 at 23:27
2

Description

I don't understand the need for a lot of what the OP has developed. But here is an example of inserting a column to the right, take the column to the left of it and copy it to the new column. Then eliminate any values or notes leaving only the formulas.

Since getFormulas() returns a 2D array of strings representing the formulas in the range simpy using setValues(formulas) places the formulas into the cells.

Code.gs

function onChange(e) {
  try {
    if( e.changeType === "INSERT_COLUMN" ) {
      let spread = SpreadsheetApp.getActiveSpreadsheet();
      let sheet = spread.getActiveSheet();
      if( sheet.getName() === "Sheet1" ) {
        // assume insert column to the right
        let colNumber = sheet.getSelection().getActiveRange().getColumn();
        if( colNumber >= 5 ) {
          let activeRng = sheet.getRange(1,colNumber,sheet.getLastRow(),1);
          let originRng = sheet.getRange(1,colNumber-1,sheet.getLastRow(),1);
          originRng.copyTo(activeRng);
          let formulas = activeRng.getFormulas();
          activeRng.setValues(formulas);
          activeRng.clearNote();
        }
      }
    }
  }
  catch(err) {
    SpreadsheetApp.getUi().alert(err);
  }
}

Reference

TheWizEd
  • 7,517
  • 2
  • 11
  • 19
  • Thank you! This fixed most of the slowness. At first I was confused why you needed `originRng.copyTo(activeRng);` at all and couldn't just do something like `activeRng.setValues(originRng.getFormulas());`, but I tried it, and the formulas referenced the wrong column. Your way worked. Given that https://stackoverflow.com/a/72586577/470749 mentions using `setFormulas` with an array, maybe the combination of your answers will be the best. I'll try that. – Ryan Jun 11 '22 at 19:25
  • Update: `activeRng.setFormulas(originRng.getFormulas());` does not work because its formulas refer to the wrong column (which is why `copyTo` is required first). https://developers.google.com/apps-script/reference/spreadsheet/range#setformulasformulas – Ryan Jun 11 '22 at 19:31