automatically move to the next cell on the page - google-apps-script

I have a spreadsheet I designed in Google Sheets to input data at work and then a formula that determines if the part needs to be replaced and provides the part number required. I need either a macro or appScript that will start with a certain cell on the same sheet, highlight it, allow me to type a value in it, then either by pressing the ENTER or TAB key to move to the next cell on the page (Not necessarily the next door cell, but a cell in another column and/or row), -AND- based on a data validation check box determine which cells are selected. How do I write either a macro or appScript to do what I need? Which would be easier?

Reference
sheet.setActiveSelection()
Script:
Try
function onEdit(event){
var sh = event.source.getActiveSheet();
var rng = event.source.getActiveRange();
if (sh.getName() == 'mySheetName'){ // adapt
var addresses = ["E7","H7","E10","H10","E13","H13","E16"]; // adapt
var values = addresses.join().split(",");
var item = values.indexOf(rng.getA1Notation());
if (item < addresses.length - 1){
sh.setActiveSelection(addresses[item + 1]);
}
}
}
Note:
This way you can determine the order in which the cells are selected.
If you have a script that copies cells into a master data sheet, you can take advantage of the range list. (by the way, you can find here how to transfer the data).
In case of protected sheet:
If your sheet is protected, except for the cells that need to be filled in, you can use a script that will search for them and reorganize them.
function onEdit(event) {
var sh = event.source.getActiveSheet();
var rng = event.source.getActiveRange();
if (sh.getName() == 'mySheetName') { // adapt
var addresses = listOfUnprotectedRanges()
var values = addresses.join().split(",");
var item = values.indexOf(rng.getA1Notation());
if (item < addresses.length - 1) {
sh.setActiveSelection(addresses[item + 1]);
}
}
}
function listOfUnprotectedRanges() {
var p = SpreadsheetApp.getActiveSheet().getProtections(SpreadsheetApp.ProtectionType.SHEET)[0];
var ranges = p.getUnprotectedRanges().map(r => [r.getA1Notation(), r.getRow(), r.getColumn()])
ranges = ranges.sort(function (a, b) { return a[2] - b[2]; }); // sort by columns
ranges = ranges.sort(function (a, b) { return a[1] - b[1]; }); // sort by ranges first
return ranges.map(r => r[0])
}

Related

Google Sheets filter values by value type

I am looking for help to create a filter on Google Sheets Script.
I want the following:
I have a database Schedule which has table with information that I want to filter
Once the database A is updated I want to filter Colum b row 8 and only take the cells that have information and are filled with words, numbers, etc.
After that I want to copy the data filtered and paste on a new Sheet Data “Foreman on specific columns
Also, I want to copy and paste the format such as color, size, etc.
I have the following code which it does the partial job but I cannot figure it out to only get the specific data needed
function Foreman(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var scheduleCCC_sheet = ss.getSheetByName("Schedule");
var Foreman_sheet = ss.getSheetByName("Foreman");
var pasteforemans = Foreman_sheet.getRange(8,2);
var originalData = scheduleCCC_sheet.getRange(9,2,scheduleCCC_sheet.getLastRow()1,11).getValues();
var filter1 ="";
Foreman_sheet.getRange(8,2,Foreman_sheet.getLastRow(),11).clearContent().clearFormat();
var data = originalData.filter(filterlogic);
Foreman_sheet.getRange(9,2,data.length,data[0].length).setValues(data);
}
var filterlogic = function(item){
if(item[1] == ""){
return false;
}else {
return true;
}
In order to filter the values based on the condition that the cells are not empty, you can use the below snippet of code:
Snippet
function myFunction() {
var ss = SpreadsheetApp.openById("ID_OF_THE_SS").getSheetByName("Schedule CCC");
var otherss = SpreadsheetApp.openById("ID_OF_THE_SS").getSheetByName("Foreman");
var range = ss.getRange("START_ROW", "START_COL", ss.getLastRow(), ss.getLastColumn());
var filter = range.getFilter();
var filterCriteria = SpreadsheetApp.newFilterCriteria().whenCellNotEmpty().build();
filter.setColumnFilterCriteria("COL_POSITION", filterCriteria);
for (var i = 1; i <= ss.getLastRow(); i++) {
if (ss.isRowHiddenByFilter(i) == false) {
ss.getRange("RANGE_FROM_THE_CCC_SHEET").copyTo(otherss.getRange("RANGE_FROM_THE_FOREMAN_SHEET"));
}
}
}
Explanation
The above code gathers the filter from the source sheet and sets it the whenCellNotEmpty criteria. Afterwards it loops through the data and checks if a specific row is hidden by the filter; if the result of this is false, then it copies the row with its format onto the destination sheet. The format of the row is preserved by using the copyTo method.
Note
You will need to adjust the ranges to match the ones in your sheet and might need to add another condition when looping through the data from the source sheet.
Reference
Apps Script Sheet Class - isRowHiddenByFilter(rowPosition);
Apps Script Filet Class - setColumnFilterCriteria(columnPosition, filterCriteria);
Apps Script Sheet Class - copyTo(spreadsheet).

Copy active cell to other cells containing string

In Google Sheets I'm trying to create a script that will take the value from the active cell and paste that value to any cell in Column B containing the string "HR". Any ideas?
This isn't too bad; you just have to wrap your head around a few concepts from Apps Script and Javascript to make it efficient. But first let's start with the naive approach!
function firstTry() {
var activeSheet = SpreadsheetApp.getActiveSheet(); // whatever is open
var activeCell = SpreadsheetApp.getCurrentCell(); // this is a single-cell range
var activeCellValue = activeCell.getValue(); // could be a string, number, etc
// Now let's look in column B for stuff to change
for (var i = 1; i <= activeSheet.getLastRow(); i++) {
var cell = activeSheet.getRange("B" + i);
var val = cell.getValue();
var valStr = String(val); // We could have gotten a number
if (valStr.indexOf("HR") != -1) {
cell.setValue(activeCellValue);
}
}
}
This will probably work, but isn't too efficient: each call to getValue() or setValue() takes some time. It'd be better to just get all the values at once, and then paste back a modified Column B when we're satisfied:
function improvement() {
var activeSheet = SpreadsheetApp.getActiveSheet(); // whatever is open
var activeCell = SpreadsheetApp.getCurrentCell(); // this is a single-cell range
var activeCellValue = activeCell.getValue(); // could be a string, number, etc
// Now let's look in column B for stuff to change
var rowsWithData = activeSheet.getLastRow() - 1;
var colBRange = activeSheet.getRange(1, // start on row 1
2, // start on column 2
rowsWithData, // this many rows
1); // just one column
// Let's get the data as an array of arrays. JS arrays are 0-based, btw
var colBData = colBRange.getValues();
for (var i = 0; i < colBData.length; i++) {
var val = colBData[i][0]; // row i, first column
var valStr = String(val); // We might have gotten a number
if (valStr.indexOf("HR") != -1) {
colBData[i][0] = activeCellValue; // modify copied data
}
}
// Lastly, write column B back out
colBRange.setValues(colBData);
}
You could go further with a fancy filter function instead of looping over the data explicitly, but that starts to get less clear.
Caveats as the OP points out in comments below, blindly calling setValues like this will pave over any formulas you have. This would have been no big deal, except that this includes hyperlinks. You could get really involved by calling getFormulas in parallel with getValues and then decide whether to call setValue or setFormula depending on the original contents of each cell.

Google Sheets move cursor onEdit trigger based on cell content

I am trying to write a Google Sheets Apps Script function that checks the content of the current active cell, matches it to the content of another cell, then moves the cursor according to the result of that check.
For a spreadsheet as this example one:
https://docs.google.com/spreadsheets/d/1kpuVT1ZkK0iOSy_nGNPxvXPTFJrX-0JgNmEev6U--5c/edit#gid=0
I would like the user to go to D2, enter a value followed by Tab, then while the active cell is in E2, the function will check if the value in D2 is the same in B2. If it is, stays in E2.
Then we enter the value in E2 followed by Tab, the function checks if it's the same as C2, if it is, then moves from F2 down and left twice to D3. So if all the values are entered correctly, the cursor zig-zags between the cells in D, E and F as shown below:
The closest I could find is the answer to the one below, but it involves clicking on a method in the menu each time:
Move sheet rows on based on their value in a given column
I imagine the function could be triggered at the beginning of editing the document, then it keeps moving the cursor until the document is completed, at which point the function can be stopped.
Any ideas?
EDIT: what I've tried so far:
I have managed to change the position to a hard-coded position 'D3' and to create a function that moves one down with these functions:
function onOpen() {
var m = SpreadsheetApp.getUi().createMenu('Move');
m.addItem('Move to D3', 'move').addToUi();
m.addItem('Move to one below', 'move2').addToUi();
m.addItem('Move down left', 'move_down_left').addToUi();
}
function move() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var range = s.getRange('D3');
s.setActiveRange(range);
}
function move2() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var r = s.getActiveRange();
var c = r.getCell(1,1);
var target = s.getRange(c.getRow() + 1, c.getColumn());
s.setActiveRange(target);
}
function move_down_left() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var r = s.getActiveRange();
var c0 = r.getCell(1,1);
var r1 = s.getRange(c0.getRow(), c0.getColumn() - 1);
var c1 = r1.getCell(1,1);
var r2 = s.getRange(c1.getRow(), c1.getColumn() - 2);
var c2 = r2.getCell(1,1);
if (c1.getValue() == c2.getValue()) {
var target = s.getRange(c1.getRow() + 1, c1.getColumn() - 1);
s.setActiveRange(target);
}
}
As I mentioned in my comment, you want to use a simple trigger function (so that it works for all users without requiring them to first authorize the script). There are naturally some limitations of simple triggers, but for the workflow you describe, they do not apply.
A key principle of a function receiving the on edit trigger invocation is that it has an event object with data about the cell(s) that were edited:
authMode:
A value from the ScriptApp.AuthMode enum.
oldValue:
Cell value prior to the edit, if any. Only available if the edited range is a single cell. Will be undefined if the cell had no previous content.
range:
A Range object, representing the cell or range of cells that were edited.
source:
A Spreadsheet object, representing the Google Sheets file to which the script is bound.
triggerUid:
ID of trigger that produced this event (installable triggers only).
user:
A User object, representing the active user, if available (depending on a complex set of security restrictions).
value:
New cell value after the edit. Only available if the edited range is a single cell.
Of these, we will use range and value. I will leave the business case of handling edits to multiple-cell ranges to you. Stack Overflow is, after all, not where you obtain turnkey solutions ;)
function onEdit(e) {
if (!e) throw new Error("You ran this from the script editor");
const edited = e.range;
if (edited.getNumRows() > 1 || edited.getNumColumns() > 1)
return; // multicell edit logic not included.
const sheet = edited.getSheet();
if (sheet.getName() !== "your workflow sheet name")
return;
// If the user edited a specific column, check if the value matches that
// in a different, specific column.
const col = edited.getColumn(),
advanceRightColumn = 5,
rightwardsCheckColumn = 2;
if (col === advanceRightColumn) {
var checkedValue = edited.offset(0, rightwardsCheckColumn - col, 1, 1).getValue();
if (checkedValue == e.value) // Strict equality may fail for numbers due to float vs int
edited.offset(0, 1, 1, 1).activate();
else
edited.activate();
return;
}
const endOfEntryColumn = 8,
endCheckColumn = 3,
startOfEntryColumn = 4;
if (col === endOfEntryColumn) {
var checkedValue = edited.offset(0, endCheckColumn - col, 1, 1).getValue();
if (checkedValue == e.value)
edited.offset(1, startOfEntryColumn - col, 1, 1).activate();
else
edited.activate();
return;
}
}
As you digest the above, you'll note that you are required to supply certain values that are particular to your own workflow, such as a sheet name, and the proper columns. The above can be modified in a fairly straightforward manner to advance rightward if the edited column is one of several columns, using either a constant offset to the respective "check" column, or an array of respectively-ordered offsets / target columns. (Such a modification would almost certainly require the use of Array#indexOf.)
A caveat I note is that strict equality === fails if your edits are numbers representable as integers, because Google Sheets will store the number as a float. Strict equality precludes type conversion by definition, and no int can ever be the exact same as a float. Thus, the generic equality == is used. The above code will not equate a blank check cell and the result of deleting content.
Method references:
Range#offset
Range#activate

Google Apps Script - set formula of active cell depending on value of another cell

I’m writing a custom function in Google Apps Script that, if a certain other cell contains a number, uses the value of that cell and several other cells to calculate a result. Otherwise, if that certain other cell does not contain a number, the function should just return an empty string so that the active cell appears blank.
I was able to come up with a working function to do this, but in order to protect sensitive information, I’m not going to copy it here. Instead, here’s an example function that accomplishes the same thing:
function myFunction() {
var sheet = SpreadsheetApp.getActiveSheet();
var rowNum = sheet.getActiveCell().getRow();
var rowVals = sheet.getRange(rowNum, 1, 1, 15).getValues();
var fVal = rowVals[0][5];
if (fVal == "" || isNaN(fVal)) {
return ""; //leave cell blank if column F doesn't contain a number
}
var aVal = rowVals[0][0];
var bVal = rowVals[0][1];
var cVal = rowVals[0][2];
var gVal = rowVals[0][6];
return ((gVal * fVal) + aVal + bVal + cVal);
}
However, in an effort to speed it up (and also some other reasons that would be complicated to try to explain here, so you'll have to just trust me), I want to have the custom function set the value of the cell to be a formula instead of doing the calculating itself. It doesn’t work to just put the formula in the cell in the first place because then it still calculates/shows a result even if column F doesn’t contain a number.
Here’s what I’ve tried so far:
function myFunction2() {
var sheet = SpreadsheetApp.getActiveSheet();
var rowNum = sheet.getActiveCell().getRow();
var fVal = sheet.getRange(rowNum, 6).getValue();
if (fVal == "" || isNaN(fVal)) {
return ""; //leave cell blank if column F doesn't contain a number
}
var formula = '=SUM((G2*F2)+A2+B2+C2)';
return formula;
}
^This just makes the cell display the string “=SUM((G2*F2)+A2+B2+C2)”.
So I then tried using setFormula on the active cell:
function myFunction3() {
var sheet = SpreadsheetApp.getActiveSheet();
var cell = sheet.getActiveCell();
var rowNum = cell.getRow();
var fVal = sheet.getRange(rowNum, 6).getValue();
if (fVal == "" || isNaN(fVal)) {
return ""; //leave cell blank if column F doesn't contain a number
}
cell.setFormula('=SUM((G2*F2)+A2+B2+C2)');
}
^which, when I called the function in a cell, returned an error saying “You do not have permission to call setFormula”. The same thing happened when I tried getting the a1 notation of the active cell and then using getRange(a1Notation).setFormula('=SUM((G2*F2)+A2+B2+C2)') instead of calling setFormula directly on the active cell.
Anybody know if there's a way around that permission error? or have any other ideas for how to accomplish this?
The permission error is because of restrictions on what user defined functions can do. You can, however, do this with onEdit like this:
function onEdit(e) {
var ss=SpreadsheetApp.getActiveSpreadsheet()
var s=ss.getActiveSheet();
var col = e.range.getColumn();
var rowNum = e.range.getRow();
if(col==6){
var fVal = s.getRange(rowNum,col,1, 1).getValue()
}
if (fVal == "" || isNaN(fVal)) {
return
}
else{
s.getRange(rowNum,col,1, 1).setFormula('=SUM((G2*F2)+A2+B2+C2)');
}}
I actually ended up figuring out a way to accomplish what I wanted using the built-in IF function, so that's what I did.

Find string and get its column

Let's say I have a lot of columns and one of them contains "impressions" string (on row 3). What I need to do is to:
1) Find the cell with "impressions" string
2) Get column number or i.e. "D"
3) Based on what I got paste a formula into i.e. D2 cell which gets AVERAGE from a range D4:D*last*
I couldn't find it anywhere so I have to ask here without any "sample" code, since I have no idea on how to achieve what I want. (3rd one is easy but I need to get that "D" first)
There's no way to search in Google Apps Script. Below is a function that will accomplish the first 2 parts for you (by iterating over every cell in row 3 and looking for "impressions"):
function findColumnNumber() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Sheet1'); // insert name of sheet here
var range = sheet.getDataRange(); // get the range representing the whole sheet
var width = range.getWidth();
// search every cell in row 3 from A3 to the last column
for (var i = 1; i <= width; i++) {
var data = range.getCell(3,i)
if (data == "impressions") {
return(i); // return the column number if we find it
}
}
return(-1); // return -1 if it doesn't exist
}
Hopefully this will allow you to accomplish what you need to do!
The indexOf method allows one to search for strings:
function findColumnNumber() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet() //whatever tab the code is run on
var data = sheet.getDataRange().getValues();
var header_row_num = 1; // TODO: change this to whichever row has the headers.
var header = data[header_row_num -1] //Remember JavaScript, like most programming languages starts counting (is indexed) at 0. For the value of header_row_num to work with a zero-index counting language like JavaScript, you need to subtract 1
//define the string you want to search for
var searchString = "impressions";
//find that string in the header and add 1 (since indexes start at zero)
var colNum = header.indexOf(searchString) + 1;
return(colNum);