This question could be rephrased to, "Using a programmatically generated range in FILTER()" depending on the approach one takes to answer it.
This question is asked for the sake of understanding how to pass a variable range into a filter function (if it's possible).
I am currently filtering using the following function:
Code Block 1
=filter('Data Import'!1:10000,'Data Import'!D:D<12)
After importing data, Column D:D can change positions (eg, it could be in column F:F), but always has the header "student.grade".
The question is: How can I reference this variable-position column with a fixed header in a filter function as in the one given in code block 1? In other words, can I replace 'Data Import'!D:D` with valid code that will allow this function to work regardless of the location of the column with header "student.grade?"
What I've tried:
I can use the following code to correctly find the address of the column (whatever it happens to be after data import) as a string:
Code Block 2
=substitute(address(1,match("student.grade",'Data Import'!1:1,0),4),1,"")&":"&substitute(address(1,match("student.grade",'Data Import'!1:1,0),4),1,"")
The function in code block 2 above returns "D:D" when the header "student.grade" is in cell D1, and "F:F" when "student.grade" is in cell F1. I thought I could simply plug this value into a FILTER() function and be on my merry way, but in order to convert my string to a usable address, I attempted to use an INDIRECT() function on the string produced in code block 2 above.
Code Block 3
=filter('Data Import'!1:3351,'Data Import'!indirect(substitute(address(1,match("student.grade",'Data Import'!1:1,0),4),1,"")&":"&substitute(address(1,match("student.grade",'Data Import'!1:1,0),4),1,""),TRUE)<12)
The formula won't parse correctly.
Simplifying the indirect portion of the same function to test whether or not it will work when given a range produces the same error:
Code Block 4
=filter('Data Import'!1:3351,indirect('Data Import'!&"D:D")<12)
This leads me to believe INDIRECT() doesn't handle ranges, or if it does, I don't know the syntax. This Stack Overflow post seems to suggest this is possible, but I can't work out the details.
This question is NOT an attempt to get others to help me solve my programming dilemma. I can do that with various scripts, giant columns of secondary if statements, and more.
This question is asked for the sake of understanding how to pass a variable range into a filter function (if it's possible).
once again, maybe this is what you want:
=FILTER('Data Import'!1:100000,
INDIRECT("'Data Import'!"&
ADDRESS(1, MATCH("student.grade", 'Data Import'!1:1, 0), 4)&":"&
ADDRESS(1000000, MATCH("student.grade", 'Data Import'!1:1, 0), 4)) < 12)
I have no idea what you want to achieve but take a look at this:
={'Data Import'!1:1;
FILTER('Data Import'!1:10000, 'Data Import'!D:D < 12)}
or:
=QUERY(FILTER('Data Import'!1:10000, 'Data Import'!D:D < 12),
"select * label Col4 'student.grade'", 0)
The OP's existing solution is based on Filter command. The challenge is that the column containing "student.grade" is not fixed, however player0 has provided an excellent formula-based solution.
An alternative might be to make use of a named range. The following code finds "student.grades" in the header (row 1) and re-defines the named range accordingly.
function so54541923() {
// setup the spreadsheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetname = "Data Import";
var sheet = ss.getSheetByName(sheetname);
// define the header row
var getlastColumn = sheet.getLastColumn();
var headerRange = sheet.getRange(1, 1, 1, getlastColumn);
Logger.log("DEBUG: Header range = " + headerRange.getA1Notation()); //DEBUG
// assign a variable for student Grades
var grades = "student.grade";
// get the headers and find the column containing "student grades"
var headerData = headerRange.getValues();
var gradesIndex = headerData[0].indexOf(grades);
// add one to the index number to account for start=zero
gradesIndex = gradesIndex + 1;
Logger.log("DEBUG: gradesIndex = " + gradesIndex); //DEBUG
// convert the column number to a letter
// assumes that student.grade will never get past column Z
var temp, letter = '';
if (gradesIndex > 0) {
temp = (gradesIndex - 1) % 26;
letter = String.fromCharCode(temp + 65) + letter;
gradesIndex = (gradesIndex - temp - 1) / 26;
}
Logger.log("DEBUG: the column is " + letter); //DEBUG
//var newrange = "'" + sheetname + "'!"+letter+":"+letter+";";
// Logger.log("the new range is "+newrange);
// get the named ranges
var namedRanges = ss.getNamedRanges();
Logger.log("DEBUG: number of ranges: " + namedRanges.length); //DEBUG
// if named range is student grades, then update range
if (namedRanges.length > 0) {
for (var i = 0; i < namedRanges.length; i++) {
var thename = namedRanges[i].getName();
Logger.log("DEBUG: Loop: i: " + i + ", and the named range is " + thename); //DEBUG
if (thename = "student.grade") {
// Logger.log("DEBUG: The named range is student.grade");//DEBUG
// set the new range based on the column found earlier
var nonstringrange = sheet.getRange("'" + sheetname + "'!" + letter + ":" + letter);
namedRanges[i].setRange(nonstringrange);
Logger.log("DEBUG: The new range is " + namedRanges[i].getRange().getA1Notation()); //DEBUG
} else {
Logger.log("DEBUG: The named range is NOT grades"); //DEBUG
}
}
}
}
Related
I am using Google ads script to check if a certain text is already exist in my spreadsheet file.
If not, I want the function to return -1, if exist return the row it was found in.
I am using a loop to run on all the cells in the column and check each one.
In order to test it, I took of the cell's data and run the test, but it didn't find them equal...
here is the log for row #5:
row # 5 contains : mobileapp::2-com.rustybrick.shabbat but we look for : mobileapp::2-com.rustybrick.shabbat
enclose the script I use:
function findInColumn(column, data)
{
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var SHEET_NAME = 'גליון 1';
var ss = SpreadsheetApp.openByUrl(SPREDSHEET_FILE);
var sheet = ss.getSheetByName(SHEET_NAME);
var column = sheet.getRange(column + ":" + column);
var lastRow = sheet.getLastRow() + 1;
var data1 = data;
var values = column.getValues();
var row = 0;
while ( values[row] !== data1 && row < lastRow )
{
row++;
if(values[row] === data1)
{
return row;
}
else
{
Logger.log("row # " + row + " contains : " + values[row] + " but we look for : " + data1);
}
}
return -1;
}
So, my two questions are:
Why the script didn't recognize it's a match?
Is there a better or quicker way to do this search? now there are only 1K rows in the file, but it will become much higher soon
Findings
After replicating your script, here's what I have found:
The var values = column.getValues() will only contain the the value on row 1 of the column because column = sheet.getRange(column + ":" + column); will only get value of range Row 1, Column 1 (e.g. the column paramter is set as 1). Thus, there's no match found.
Solution
If I understand it correctly, here are your goals:
Check & find a match on each cells of a column
Return the row # of the matched cell OR return -1 if there's no match found
Find a better or quicker way to do the search
You may try this simpler script:
A switch statement was used on the sample script instead of an if statement as it is more efficient, given you will have more than 1k of total rows. See
function findInColumn(column, data)
{
var ss = SpreadsheetApp.openByUrl(SPREDSHEET_FILE_URL).getSheetByName('גליון 1');
var total_rows = ss.getDataRange().getValues().length;
var result = -1;
Logger.log("==================\nCHECKING COLUMN #"+column + "\n==================\nDATA TO FIND:\n"+data+"\n==================");
for(row=1; row<=total_rows; row++){
var currentRowValue = ss.getRange(row,column).getValue();
switch(currentRowValue) {
case currentRowValue = data:
Logger.log("Found a match on row #"+row);
result = row;
break;
}
}
return result;
}
Test Results
1. Sample גליון 1 Sheet with test values on Column 1:
2. Ran the findInColumn(column, data) with 1 &
"mobileapp::2-com.rustybrick.shabbat" as parameter values on a testing function and the result returned row 3.0 :
3. If there will be no match found, the returned result will be -1.0:
I am trying to Apply a formula to a column but only if the cell does not contain a certain value (EMAIL SENT), I have a script that sends emails when a row has "send email" in column "AF" and replaces it with "EMAIL SENT" to avoid re-sends, I need it to skip cells with "EMAIL SENT" in them.
Here is the script I have so far but it rewrites over cells and resets cells back to "send email".
Thanks in advance.
A Beginner scripter.
ps or to get this formula to start at the first empty cell in column AF.
var sss4 = SpreadsheetApp.openById('<ID>');
var ss4 = sss4.getSheetByName('Results');
ss4.getRange("AF2").setFormula('=IF(((AND(OR(I2="YOUR TRIP DWL",I2="MEGA PACK DWL (YT + AA + BONUS)"),M2<=0,AA2<>"",AE2<>""))), "Send Email", "Wait")');
var lr4 = ss4. getLastRow();
var filldownrange4 = ss4.getRange(2, 32, lr4-1);
ss4. getRange("AF2").copyTo(filldownrange4);
Rather than insert a formula by script, I suggest putting the formula directly into the spreadsheet. This would greatly simplify your code.
I took your formula and added some elements:
1 - Turned Column AF in to a column that gets updated when an email is sent.
2 - Added a "Status" Column (maybe Column AG??). This is where the formula goes.
3 - Converted the formula to ARRAYFORMULA. Note that each range in the formula includes an entire column (for example I2:I). This also required replacing AND with "*" and OR with "+". Refer below for an explanation.
4 - Added ARRAY_CONSTRAIN so that the formula is added only as you add a new row of data. If this wasn't here, then the formula would display in every row of the column to the bottom of the sheet - not a great look and a bit off-putting.
=ARRAY_CONSTRAIN(ArrayFormula(if(M2:M="EMAIL SENT","EMAIL SENT",IF(((I2:I="YOUR TRIP DWL")+(I2:I="MEGA PACK DWL (YT + AA + BONUS)"))*(J2:J<=0)*(K2:K<>"")*(L2:L<>""), "Send Email", "Wait"))),COUNTA(I2:I),1)
Here's a screenshot of my layout and formula results.
The logic works like this:
Check Column M (your Column AF). Evaluate whether an email has already been sent. If yes then do nothing; if no then evaluate whether it is time to send an email.
If an email hasn't been sent yet, then evaluate the status of certain fields.
Column I (your column I) - does it contain either 'YOUR TRIP DWL' or 'MEGA PACK DWL (YT + AA + BONUS)'. If yes, then continue, otherwise 'Wait'.
Check that each of the following columns evaluate as true.
Column J (your Column M) <=0 (less than or equal to zero)
Column K (your Column AA) <>"" (isn't blank)
Column L (your Column AE) <>"" (isn't blank)
If these all evaluate as true, then 'Send an email', otherwise 'Wait'.
The number of rows in which the formula results are displayed is controlled by ARRAY_CONSTRAIN and the parameter COUNTA(I2:I). This counts the number of rows with content in Column I (your Column I). Choosing this column was on the assumption that there would always be a value in Column L. If that's not the case, then choose another cell/column that will always have content. You may have to alter COUNTA to COUNT if you choose a cell/column with numeric values.
ARRAYFORMULA The formula is entered into one cell only (the equivalent of Cell AG2 in the "Status" Column). ARRAYFORMULA evaluates the formula on a row-by-row and automatically displays the results in every row of the Column. ARRAY_CONSTRAIN constrains the results; in our case, it is limited to just those rows that have a value.
What happened to AND and OR in the IF statement?
Note that the IF statement doesn't literally include the AND and OR functions. Apparently, they have a nullifying effect on ARRAYFORMULA. BUT credit to Max Makhrov in "Google Sheets ARRAYFORMULA with nested if statements" where he advised.
Replace AND with * and OR with +
Implications for your code
Your code should loop through and evaluate the "Status" Column.
If the value is "Send Email", then send an email and update the "Status" column to "EMAIL SENT". Otherwise, do nothing.
UPDATE: Code changes
This is the Code that would go with the formula proposed. The code is self-documented and reasonably easy to follow. The broad flow is processing is as follows.
1 - allow for user variables
2 - remotely open the spreadsheet
3 - calculate the number of rows of data
4 - loop through the data by row
5 - if cell value = "Send Email", then send email, and update value of adjacent column with "EMAIL SENT"
function so_52680239() {
// Declare user variables
// SpreadsheetID
var ResultsSheetID = "1Iz-qmOnzZp4EAmGzWJORNpJGkueYzGFUTkpUZ9g3-as";
// SheetName
var SheetName = "Results";
// Column letter for the Status Column
var StatusColumnLetter = "N";
// Start row for the Status Column
var StatusHeaderRow = 1;
// open the Results Spreadsheet
// Supply the ID
var ResultsSheet = SpreadsheetApp.openById(ResultsSheetID);
var Results_ss = ResultsSheet.getSheetByName(SheetName);
// Convert the column letter for Status Column to a number
var StatusColumnNumber = letterToColumn(StatusColumnLetter);
//Logger.log("StatusColumnNumber = "+StatusColumnNumber);
// Create a string variable range for the Status Column
var StatusStartRange = StatusColumnLetter+StatusHeaderRow+":"+StatusColumnLetter;
//Logger.log("StatusStartRange = "+StatusStartRange);
// Get the last row with content in the StatusColumn
var LastRowvals = Results_ss.getRange(StatusStartRange).getValues();
var LastDataRow = LastRowvals.filter(String).length;
//Logger.log('Last row = '+LastDataRow);
// declare the search range
var searchRange = Results_ss.getRange(StatusHeaderRow+1,StatusColumnNumber, LastDataRow-1);
//var searchRangeA1 = searchRange.getA1Notation();
//Logger.log('searchRangeA1 = '+searchRangeA1);
// Get array of values in the search Range
var rangeValues = searchRange.getValues();
// Loop through array and if condition met, send email
// Note, only one column of data so no looping by column
for ( j = 0 ; j < LastDataRow - 1; j++){
if(rangeValues[j][0] === 'Send Email'){
// do stuff
// insert code to send email
//Logger.log("j = "+j+", value = "+rangeValues[j][0]+", so Send Email");
// Update your Column AF (one column to the left of the formula results column) with "EMAIL SENT"
Results_ss.getRange(StatusHeaderRow+1+j,StatusColumnNumber).offset(0, -1).setValue('EMAIL SENT');
}else {
// do Nothing
//Logger.log("j = "+j+", value = "+rangeValues[j][0]+", so do nothing");
};
};
}
//Utility to convert a column letter to a column number
function letterToColumn(letter)
{
var column = 0, length = letter.length;
for (var i = 0; i < length; i++)
{
column += (letter.charCodeAt(i) - 64) * Math.pow(26, length - i - 1);
}
return column;
}
Screenshot - AFTER running the code. Note record#2 is updated.
Credit:
yagisanatode.com for fast/efficient way to update data: Google Apps Script – Iterating Through Ranges in Sheets the Right and Wrong Way
Code Only Solution
In the event that the spreadsheet can't be manually updated for the formula, a script can do the same task.
Assumption: Column I always contains data; this is used to calculate how many rows of data to be processed.
Provisio: If, at the time of running the script, the relevant cell in AF2 is empty, then the formula will be inserted in that cell. The formula will then evaluate the data and the result may be that it displays the "Send Mail" value. However, there is no provision to then send the mail during the same update routine.
I'll leave it to the Questioner as to how they choose to deal with that matter.
function so_52680239_02() {
// Declare user variables
// SpreadsheetID
var ResultsSheetID = "1ILmQ5cAwRD0Va0lPKcAhr7OTvwLaX_UtFCJ8EEL4TwM";
// SheetName
var SheetName = "Results01";
// Column letter for the Status Column
var StatusColumnLetter = "M";
// Start row for the Status Column
var StatusHeaderRow = 1;
// Column letter for the TripType (assume that there is always data in this cell)
var TripTypeColumnLetter = "I";
// open the Results Spreadsheet
// Supply the ID
var ResultsSheet = SpreadsheetApp.openById(ResultsSheetID);
var Results_ss = ResultsSheet.getSheetByName(SheetName);
// Convert the column letter for Status Column to a number
var StatusColumnNumber = letterToColumn(StatusColumnLetter);
// Logger.log("StatusColumnNumber = "+StatusColumnNumber); //DEBUG
// Convert the column letter for Trip Type Column to a number
var TripTypeColumnNumber = letterToColumn(TripTypeColumnLetter);
// Logger.log("TripTypeColumnNumber = "+TripTypeColumnNumber);// DEBUG
// Create a string variable range for the Status Column
var StatusStartRange = StatusColumnLetter + StatusHeaderRow + ":" + StatusColumnLetter;
// Logger.log("StatusStartRange = "+StatusStartRange); // DEBUG
// Create a string variable range for the Trip Type Column
var TripTypeRange = TripTypeColumnLetter + StatusHeaderRow + ":" + TripTypeColumnLetter;
// Logger.log("TripTypeRange = "+TripTypeRange); // DEBUG
// Get the last row with content in the Trip Type Column
var TripLastRowvals = Results_ss.getRange(TripTypeRange).getValues();
var TripLastDataRow = TripLastRowvals.filter(String).length;
// Logger.log('Last trip row = '+TripLastDataRow);// DEBUG
// Define the formula
var myformula = '=IF(((AND(OR(I' + row + '="YOUR TRIP DWL",I' + row + '="MEGA PACK DWL (YT + AA + BONUS)"),M' + row + '<=0,AA' + row + '<>"",AE' + row + '<>""))), "Send Email", "Wait")';
// declare the search range in Column AF
var searchRange = Results_ss.getRange(StatusHeaderRow + 1, StatusColumnNumber, TripLastDataRow - 1);
// Logger.log('searchRange = '+searchRange.searchRangeA1); //DEBUG
//Get array of values in the search Range
var rangeValues = searchRange.getValues();
//Logger.log("rangevalues = "+rangeValues)
// establish some variablkes for use in the loop
var emailformula = "";
var emailformularange = "";
// loop for email formula
for (var row = 0; row < TripLastDataRow - 1; row++) {
if (rangeValues[row][0] == "") {
// cell is empty, insert formula
emailformula = '=IF(((I' + (row + 2) + '="YOUR TRIP DWL")+(I' + (row + 2) + '="MEGA PACK DWL (YT + AA + BONUS)"))*(J' + (row + 2) + '<=0)*(K' + (row + 2) + '<>"")*(L' + (row + 2) + '<>""), "Send Email", "Wait")';
emailformularange = '' + StatusColumnLetter + (row + 2) + ''
var rangeList = Results_ss.getRangeList([emailformularange]);
rangeList.setFormula(emailformula);
//Logger.log("row = "+row+", value = "+rangeValues[row][0]+", Action: Cell is empty, so insert formula in "+emailformularange);//DEBUG
} else if (rangeValues[row][0] == "EMAIL SENT") {
//do nothing
//Logger.log("row = "+row+", value = "+rangeValues[row][0]+", Action: Email already sent so do nothing"); // DEBUG
} else if (rangeValues[row][0] == "Wait") {
//do nothing
//Logger.log("row = "+row+", value = "+rangeValues[row][0]+", Action: Wait, so do nothing"); //DEBUG
} else if (rangeValues[row][0] == "Send Email") {
// Send mail
emailformularange = '' + StatusColumnLetter + (row + 2) + ''
Results_ss.getRange(emailformularange).setValue('EMAIL SENT');
// Logger.log("row = "+row+", value = "+rangeValues[row][0]+", Action: Send Mail and update cell "+emailformularange+" to 'EMAIL SENT'"); //DEBUG
}
}
}
I have the following functions. The first, lastRowF, finds the last row in a column that you give it. It works perfectly.
//Finds last row in a given column
function lastRowF(column) {
var lastRow = SpreadsheetApp.getActiveSheet().getMaxRows();
var values = SpreadsheetApp.getActiveSheet().getRange(column + "1:" + column + lastRow).getValues();
for (; values[lastRow - 1] == "" && lastRow > 0; lastRow--) {}
return lastRow;
}
The second, lastColF, does the same thing but for columns in a given row. Or at least it should.
//Finds last column in a given row
function lastColF(row) {
var lastCol = SpreadsheetApp.getActiveSheet().getMaxColumns();
// var values = SpreadsheetApp.getActiveSheet().getRange("A" + row + ":" + colLetter(lastCol) + row).getValues();
var values = SpreadsheetApp.getActiveSheet().getRange("A2:AB2").getValues();
for (; values[lastCol - 1] == "" && lastCol > 0; lastCol--) {}
return colLetter(lastCol);
}
As it is, the function simply returns lastCol as it's originally declared. The for loops is exited immediately.
I've determined that values is the issue, but I cannot figure out WHY, and it's driving me mad. The array populates correctly - msgBox(values) lists all of the values in the row - but attempting to call an individual value simply returns undefined. It doesn't make any sense to me because checking the whole array and individual values in lastRowF works perfectly fine.
I'm not sure if this assists the questioner. In any event, it is my favourite bit of code from Stack Overflow (credit to Mogsdad who in his turn adapted it from another answer to a different question. This is the power of Stack Overflow in action.)
To determine the last row in a single column - (done in 2 lines of code):
var Avals = ss.getRange("A1:A").getValues();
var Alast = Avals.filter(String).length;
You can do the same thing for Columns by changing the direction of the range.
I've been having a hard time trying to figure this out. I realize this is perhaps more basic than usual for those who follow the GAS tag, however any help much appreciated.
If I'm breaking up my bigger task into component parts, my goal right now with this question is to update several named ranges automatically.
There is a tab on the spreadsheet called "DataImport". DataImport has 10 columns all 1000 rows long. There is a named range for each column e.g. cabbages (A2:A1000), dogs (B2:B1000) etc etc.
There is a script attached to a new menu item "Update Data" that when selected imports a csv file into DataImport tab meaning that the length of the data set will grow.
How can I tell the sheet to update each named range to be the length of data? So if the original named range "cabbages" was A2:A1000 and following an update the data now actually goes as long as A2:A1500, how would I tell the sheet to update the range cabbages?
I found a snippet of code online and started to fiddle with it:
function testNamedRange() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var range = ss.getRange('DataImport!A:A');
var data_len = range.length;
SpreadsheetApp.getUi().alert(data_len); // "alert just gives "undefined"
ss.setNamedRange('TestRange', range);
var rangeCheck = ss.getRangeByName('TestRange');
var rangeCheckName = rangeCheck.getA1Notation();
}
My thinking was if I could just get the length of data following an update using the custom menu function, I could then use setNamedRange() to update cabbages range.
I'm really lost and I imagine this is simpler than I'm making it out to be.
How can I update the named range cabbages to be the length of data in UpdateData column A?
Edit: IMPORTANT
Use INDIRECT("rangeName") in formulas instead of just rangeName.
The only way to extend the range programmatically is by removing it and then adding it back with a new definition. This process breaks the formula and returns #ref instead of the range name. This should be an unnecessary work around. if you agree please star and the issue tracker at: https://code.google.com/p/google-apps-script-issues/issues/detail?id=5048
=sum(indirect("test1"),indirect("test3"))
Emulates open ended named ranges by checking to see that the last row in the named range is the same as the last row in the sheet. If not, adjusts the named range so the last row in the named range is the same as the last row in the sheet.
should probably be used with on open and on change events.
function updateOpenEndedNamedRanges() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// names of open-ended ranges
var openEndedRangeNames = ["test1", "test2", "test3", "s2test1" ];
for(i in openEndedRangeNames) {
var rName = openEndedRangeNames[i];
try{
var r = ss.getRangeByName(rName);
}
catch(err) {
GmailApp.sendEmail("me#gmail.com",
rName + " -- Cannot find",
"Trying to update open-ended ranges after rows added. \n"
+ "Unable to find range name-- "+ rName
+ " -- in ss ( " + ss.getName() + " ) "
+ "\n If it is not needed please remove it "
+ "from array \n openEndedRangeNames[] \n in the function \n"
+ "updateOpenEndedNamedRanges()");
continue;
}
var rlr = r.getLastRow();
var s = r.getSheet();
var slr = s.getMaxRows();
if(rlr==slr ) continue;
var rfr = r.getRow();
var rfc = r.getColumn();
var rnc = r.getNumColumns();
var rnr = slr - rfr + 1;
ss.removeNamedRange(rName);
ss.setNamedRange( rName, s.getRange(rfr, rfc, rnr, rnc ));
}
}
function ssChangeEvent(change) {
// changeType (EDIT, INSERT_ROW, INSERT_COLUMN, REMOVE_ROW,
// REMOVE_COLUMN, INSERT_GRID, REMOVE_GRID, or OTHER)
switch(change.changeType) {
case "INSERT_ROW":
updateOpenEndedNamedRanges();
break;
default:
Logger.log(change.changeType + " detected. No action taken ");
}
}
Setup ssChangeEvent(change) to run when rows are added
Resources>this projects triggers
Offering this function I wrote to handle dynamic resize of named ranges:
function resizeNamedRange(rangeName, addRows, addCols) {
/* expands (or decreases) a range of a named range.
rows and columns to add can be negative (to decrease range of name). Params:
rangeName - name of range to resize.
addRows - number of rows to add (subtract) from current range.
addCols - number of columns to add (subtract) from current range.
Call example: resizeNamedRange("Products",1,0);
*/
var sh = SpreadsheetApp.getActiveSpreadsheet();
try {
var oldRange = sh.getRangeByName(rangeName);
var numRows = oldRange.getNumRows() + addRows;
var numCols = oldRange.getNumColumns() + addCols;
if (numRows < 1 || numCols <1) {
Logger.log("Can't resize a named range: minimum range size is 1x1.");
return;
}
sh.setNamedRange(rangeName, oldRange.offset(0,0,numRows, numCols));
} catch (e) {
Logger.log ("Failed resizing named range: %s. Make sure range name exists.", rangeName);
}
}
Maybe I'm missing something, but the function below takes a rangename and the range that it should contain. If the rangename already exists it updates the range to the passed value. If the rangename doesn't exist, it creates it with the passed range values.
Also with regard to the "#REF!" problem in the sheet. You can do a find and replace and tick the box for "find in formulas". Put "#REF!" in find and the named range name in the replace box. This assumes only one named range was deleted and that there were no other unrelated #REF! errors. This approach helped me fix a spreadsheet with the error spread over 8 different sheets and 20+ formulas in just a few minutes.
/**
* Corrects a named range to reflect the passed range or creates it if it doesn't exist.
*
* #param {string} String name of the Named Range
* #param {range} Range (not string, but range type from Sheet class)
* #return {void || range} returns void if Named Range had to be created, returns NamedRange class if just updated. This needs improvement.
* #customfunction
*/
function fixNamedRange (name, range) {
var i = 0;
var ss = SpreadsheetApp
.getActiveSpreadsheet();
var ssNamedRanges = ss.getNamedRanges();
for (i = 0; i<ssNamedRanges.length && ssNamedRanges[i].getName() != name; i++) {};
if (i == ssNamedRanges.length) {
return (ss.setNamedRange(name, range));
} else {
return (ssNamedRanges[i].setRange(range));
}
}
I found the solution!
I have a cell with a drop down list with all the clients that the company has registered on the system, if the name we enter does not appear on the list, then function newClient executes. Basically we use the SpreadsheetApp.getUi() in order to save the new information. Once we have introduced the client data, function creates a new row on the client's sheet and enters the information from the prompts on the last row. Once done, updates the drop down list automatically.
The real function is inside of a big function that calls newClient if it's needed so the real one would be newClient(client, clients), on the example I put the variables in order to make it easier.
I hope it works!
function newClient() {
var ss = SpreadsheetApp.getActive().getSheetByName('Clients'); // Sheet with all the client information, name, city, country...
var client = 'New company';
var clients = ss.getRange('A2:A').getValues();
var ui = SpreadsheetApp.getUi();
ui.alert('Client '+client+' does not exist, enter the next information.');
var city = ui.prompt('Enter city').getResponseText();
var country = ui.prompt('Enter country').getResponseText();
client = client.toUpperCase();
city = city.toUpperCase();
country = country.toUpperCase();
ui.alert('Here is the information you entered about '+client+':'+'\n\n'+'City: '+city+'\n\n'+'Country: '+country)
ss.insertRowAfter(ss.getLastRow()); // Insert a row after the last client
ss.getRange('A'+(clients.length+2)).setValue(client); // Let's suppose we have 150 clients, on the first row we have the titles Client, City, Country, then we have the 150 clients so the last client is on row 151, that's why we enter the new one on the 152
ss.getRange('B'+(clients.length+2)).setValue(city);
ss.getRange('C'+(clients.length+2)).setValue(country);
var namedRanges = SpreadsheetApp.getActive().getNamedRanges(); // We get all the named ranges in an array
for (var i = 0; i < namedRanges.length; i++) {
var name = namedRanges[0].getName();
if (name == 'Clients') { // All the clients are stored on 'Clients' named range
var range = ss.getRange('A2:A'); // Update the range of the 'Clients' named range
namedRanges[i].setRange(range);
}
}
ui.alert('Client created, you can find it on the drop down list.');
}
How would I format non-contiguous columns to be copied to another sheet? I know (thanks Serge) that you can do contiguous columns with the following!
.getRange("A2:C")
say I need to do column A, C, K, AD, BB for example.
Is there a simpler way than assigning all columns you need different variables, getting them all individually, and putting them in the sheet you need?
Thanks for the help!
Probably not simpler, but I would say better performance, to get one big range encompassing all the columns you need with .get(Data)Range().getValues(), use Javascript to strip down the array to only the columns you need, and use setValues() to paste the values in one hit:
function copyValuesOnly() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var values = ss.getSheetByName('Source').getDataRange().getValues();
values.shift(); //remove header row
var columns = [0, 2, 10, 29, 53]; //array of zero-based indices of required columns
var output = [];
for (var i = 0, length = values.length; i < length; i++) {
output[i] = [];
for (var j = 0, width = columns.length; j < width; j++) {
output[i][j] = values[i][columns[j]];
}
}
ss.getSheetByName('Destination').getRange(2, 1, length, width).setValues(output);
}
The issue would be if you required copying formats and formulae as well, in which case the best option might be copy-pasting each column individually, as you mentioned.
My answer is really a little redundant/academic, as the =QUERY() function will allow you to do the what you want. eg =QUERY(A1:D31,"Select C, A, B") I've also given an example of using it on the example sheet (linked below). QUERY can also be used with =IMPORTRANGE() explanation from #AdamL. I've included that functionality in my function too to demonstrate. Finally, my function can be used in a spreadsheet, or in a script with no modifications. There are examples of using QUERY(IMPORTRANGE()) and my function copyColumns at my example spreadsheet.
I have included some validation so that the function can be used by less technical folks using spreadsheets. Hopefully it's useful to you too. I have made liberal use of JS functionality including RegExp, Array.map, and the Conditional Operator please ask for any clarity you need in the comments here.
The basics: It takes a string of the form "SheetName!A,C,B" where the SheetName! is optional. And it can take a starting row, with a default of 1. It can also deal with non local spreadsheets by being given a sheetKey (with or without starting row).
For example: =copyCoumns("MyDataSheet!C,A,W",8) Will copy the columns C, A and W in that order starting with row 8.
Here's the function! Enjoy!
function copyColumns(sourceRange,start,sheetKey) {
// Initialize optional parameter
if(!sheetKey && typeof start !== "number") {
sheetKey = start;
start = 1;
} else {
start = start || 1;
}
// Check SourceRange Input
var inputRe = /^((.*?!)(?=[a-z],?|[a-i][a-z]))?[a-i]?[a-z](,[a-i]?[a-z])*$/i;
if(!inputRe.test(sourceRange))
throw "Invalid SourceRange: " + sourceRange;
// Check Start Row
if(typeof start !== "number")
throw "Starting row must be a number! Got: " + start;
if(start % 1 !== 0)
throw "Starting row must be an integer! Got: " + start;
if(start < 1)
throw "Starting row can't be less than 1! Got: " + start;
// Get the Source Sheet
try {
var ss = sheetKey
? SpreadsheetApp.openById(sheetKey)
: SpreadsheetApp.getActiveSpreadsheet();
} catch(err) {
throw "Problem getting sheet" + sheetKey + " - " + err;
}
var sheetName = sourceRange.match(/^.*?(?=!)/);
var sheet = sheetName
? ss.getSheetByName(sheetName[0])
: ss.getActiveSheet();
// Check that everything is still valid
if(!sheet)
throw "Could not find sheet with name: " + sheetName;
if(start > sheet.getLastRow())
throw "No data beyond row: " + start + " Last row: " + sheet.getLastRow();
// Get the values
var lastCol = sheet.getLastColumn();
var lastRow = sheet.getLastRow()-start+1;
var values = sheet.getRange(start,1,lastRow,lastCol).getValues();
// Get the desired columns from the string
var desiredColMatch = sourceRange.match(/([a-i]?[a-z](,[a-i]?[a-z])*)$/i);
var desiredColumns = desiredColMatch[0].toUpperCase().split(",");
// In case the column we are trying to grab doesn't exist in the sheet
var lastColId = sheet.getMaxColumns() - 1; // Array is 0 indexed, Sheet is 1
// Get the numerical values of the passed in Column Ids
var columns = desiredColumns.map(function(colId){
var num = colId.length - 1; // 0 or 1
var colNum = colId.charCodeAt(num)-65+num*26*(colId.charCodeAt(0)-64);
if(colNum > lastColId)
throw "Invalid Column: " + colId + " - Column not in: " + sheetName;
return colNum;
});
//Map the values to a new array of just the columns we want
return values.map(function(row){
return columns.map(function(col){
return row[col]
})
});
}