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]
})
});
}
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:
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
}
}
}
}
I need a help with a cell-deletion script. In general, I want to run a reset script that clears out all of the data up to the day I run it. Because I am statically inputting values into those cells that are matching up with information from a filter, I believe I need to delete those cells to properly line up my inputs with where the filter information will be after I delete the expired rows from the exporting page.
Here's what I want to do in my script: If the Column F value < today's date, then delete the cells in I, J, and K and shift the cells below them up. I think I found code to do this, but it takes so long to run that the program times out before it can get through more than a few rows. I will use a for loop to run it over 73 pages, so if it is lagging out on one...yeah, I need help!
function deleteEntries() {
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var datarange = ss.getDataRange();
var lastrow = datarange.getLastRow();
var values = datarange.getValues();
var currentDate = new Date();
for (i = lastrow; i >= 5; i--) {
var tempdate = values[i-1][5];
if (tempdate < currentDate)
{
ss.getRange(i-1, 8).deleteCells(SpreadsheetApp.Dimension.ROWS);
ss.getRange(i-1, 9).deleteCells(SpreadsheetApp.Dimension.ROWS);
ss.getRange(i-1, 10).deleteCells(SpreadsheetApp.Dimension.ROWS);
}}}
In accordance with Apps Script "best practices", you will want to limit the use of the Spreadsheet Service to improve execution times. There are two "immediate" optimizations that can be considered:
Delete more than 1 cell at a time in a row
To do this, simply select a 1-row x 3-column range: ss.getRange(i-1, 8, 1, 3) instead of selecting (i-1, 8), (i-1, 9), (i-1, 10) and calling deleteCells on each of the three Ranges.
Sort your sheet before deleting such that only 1 delete call is necessary (e.g. the C++ stdlib "erase-remove" idiom). If your data is sorted based on column F, such that all data that should be removed is at the end, then you simply need to iterate the in-memory array (a very fast process) to locate the first date that should be removed, and then remove all the data below & including it.
An implementation of option 2 would look like this (I assume you use frozen headers, as they do not move when the sheet or range is sorted).
function sortDescAndGetValuesBack_(s, col) {
return s.getDataRange().sort({column: col, ascending: false}).getValues();
}
function deleteAllOldData() {
const sheets = SpreadsheetApp.getActive().getSheets()
.filter(function (sheet) { /** some logic to remove sheets that this shouldn't happen on */});
const now = new Date();
const dim = SpreadsheetApp.Dimension.ROWS;
sheets.forEach(function (sheet) {
var values = sortDescAndGetValuesBack_(sheet, 6); // Col 6 = Column F
for (var i = sheet.getFrozenRows(), len = values.length; i < len; ++i) {
var fVal = values[i][5]; // Array index 5 = Column 6
if (fVal && fVal < now) { // if equality checked, .getTime() is needed
console.log({message: "Found first Col F value less than current time",
index: i, num2del: len - i, firstDelRow: values[i],
currentTime: now, sheet: sheet.getName()});
var delRange = sheet.getRange(1 + i, 8, sheet.getLastRow() - i, 3);
console.log({message: "Deleting range '" + sheet.getName() + "!" + delRange.getA1Notation() + "'"});
delRange.deleteCells(dim);
break; // nothing left to do on this sheet.
}
}
console.log("Processed sheet '" + sheet.getName() + "'");
});
}
References:
Array#filter
Array#forEach
Range#sort
Range#deleteCells
I have a model in Google Sheets that is set up with one column per day. It contains both actuals and forecasts, and every day I need to roll forward formulas to replace forecasts with actuals. I can't roll forward the whole column, only a segment of it (there are reference numbers above and below that shouldn't be changed).
I have tried to write a script to do this for me every day, but I don't know how to make getRange reference a dynamic range. This is my attempt:
function rollColumn() {
var ss2 = SpreadsheetApp.openById('<ID redacted>');
ss2.getRange("=index(Model!$7:$7,,match(today()-2,Model!$4:$4,0)):index(Model!$168:$168,,match(today()-2,Model!$4:$4,0))").copyTo(ss2.getRange("=index(Model!$7:$7,,match(today()-1,Model!$4:$4,0)):index(Model!$168:$168,,match(today()-1,Model!$4:$4,0))"))
};
The INDEX formulas work insofar as they reference the relevant ranges (I have tested them in the spreadsheet). But clearly getRange doesn't accept formulas as an input. It also seems that Google Sheets doesn't allow for a named range to be created with formulas (which is how I would solve this in Excel).
Can someone help me recreate this functionality with GAS?
This is the closest existing question I've found on Stack Overflow, but I haven't been able to make it work:
Google Apps Script performing Index & Match function between two separate Google Sheets
Thank you!
You should add {contentsOnly:false} parameter to your code. something like this:
TemplateSheet.getRange("S2:T2").copyTo(DestSheet.getRange("S2:T"+LRow2+""), {contentsOnly:false});
Getting a date from column's title, then pasting formulas to the row to the right:
// note: we assume that sheet is disposed as in the following document: https://docs.google.com/spreadsheets/d/1BU2rhAZGOLYgzgSAdEz4fJkxEcPRpwl_TZ1SR5F0y08/edit?ts=5a32fcc5#gid=0
function find_3formulas() {
var sheet = SpreadsheetApp.getActiveSheet(),
leftTitle, // this variable will stay unused because we do not need a vertical index
topTitle = todayMinus_xDays(2),
topTitlesRange = sheet.getRange("G3:T3"),
leftTitlesRange = sheet.getRange("A4:A8"); // this range will stay unused.
var coor = findCoordinates(leftTitlesRange, leftTitle, topTitlesRange, topTitle);
if (coor.row == null || coor.column == null) {
sheet.getRange("M12:M14").setFormula('="NULL: please check logs"');
return;
}
var rowAxis = 4 + coor.row;
var colAxis = 8 + coor.column;
var fromRange = sheet.getRange(rowAxis, colAxis, 3, 1);
var toRange = sheet.getRange(rowAxis, colAxis + 1, 3, 1);
Logger.log(fromRange.getA1Notation())
Logger.log(toRange.getA1Notation());
var threeFormulas = fromRange.getFormulas();
toRange.setFormulas(threeFormulas)
}
// unused in current script!
function findCoordinates(leftTitlesRange, leftTitle, topTitlesRange, topTitle) {
var formattedDate,
row = 0,
column = 0;
if (leftTitle) {
row = findRow(leftTitlesRange, leftTitle);
}
if (topTitle) {
column = findColumn(topTitlesRange, topTitle);
}
var array = {row:row, column:column}
return array;
}
// unused in current script!
function findRow(range, valueToSearch) {
var colRows = range.getValues();
for (i = 0; i < colRows.length; i++) {
if (valueToSearch == colRows[i][0]) {return i;}
}
// however, if found nothing:
Logger.log("the value " + valueToSearch + " could not be found in row titles");
return null;
}
// assumes that column titles are dates, therefore of type object.
function findColumn(range, valueToSearch) {
var colTitles = range.getValues();
for (i = 0; i < colTitles[0].length; i++) {
if (typeof colTitles[0][i] == "object") {
formattedDate = Utilities.formatDate(colTitles[0][i], "GMT", "yyyy-MM-dd")
};
if (valueToSearch === formattedDate) {return i;}
}
// however, if found nothing:
Logger.log("today's date, " + valueToSearch + ", could not be found in column titles");
return null;
}
// substracts 2 days from today, then returns the result in string format.
function todayMinus_xDays(x) {
var d = new Date();
d = new Date(d - x * 24 * 60 * 60 * 1000);
d = Utilities.formatDate(d, "GMT", "yyyy-MM-dd");
return d;
}
i have this script to delete certain rows, if the selected cell in the selected colum has the metioned content but i don't understand where it fails
function DeleteRowByKeyword() {
var value_to_check = Browser.inputBox("Enter the keyword to trigger delete Row","", Browser.Buttons.OK);
// prendo quello attivo
var DATA_SHEET = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var FIRST_COLUMN = Browser.inputBox("Number of Column to look at (eg: for column A enter 1)","", Browser.Buttons.OK);
ss.toast("removing duplicates...","",-1);
var dataCopy1 = DATA_SHEET.getDataRange().getValues();
var deleted_rows = 0;
var rangeToCopy = '';
if (dataCopy1.length > 0) {
var i = 1;
while (i < DATA_SHEET.getMaxRows() - deleted_rows) {
if ((dataCopy1[i][FIRST_COLUMN]).search(value_to_check) != -1) {
ss.deleteRow(i);
deleted_rows++;
}
i++;
}
}
ss.toast("Done! " + deleted_rows + ' rows removed',"",5);
}
thanks in advance for any help
There are a few things to be improved:
Remember that spreadsheet rows and columns are numbered starting at
1, for all methods in the SpreadsheetApp, while javascript arrays
start numbering from 0. You need to adjust between those numeric
bases when working with both.
The String.search() method may be an inappropriate choice here, for
two reasons. The .search() will match substrings, so
('Testing').search('i') finds a match; you may want to look for exact
matches instead. Also, .search() includes support for regex
matching, so users may be surprised to find their input interpreted
as regex; .indexOf() may be a better choice.
To limit operations to rows that contain data, use .getLastRow()
instead of .getMaxRows().
When deleting rows, the size of the spreadsheet dataRange will get
smaller; you did take that into account, but because you're looping
up to the maximum size, the code is complicated by this requirement.
You can simplify things by looping down from the maximum.
The input of a Column Number is error-prone, so let the user enter a
letter; you can convert it to a number.
You had not defined a value for ss (within this function).
Here's the updated code:
function DeleteRowByKeyword() {
var value_to_check = Browser.inputBox("Enter the keyword to trigger delete Row", "", Browser.Buttons.OK);
var matchCol = Browser.inputBox("Column Letter to look at", "", Browser.Buttons.OK);
var FIRST_COLUMN = (matchCol.toUpperCase().charCodeAt(0) - 'A'.charCodeAt(0) + 1); // Convert, e.g. "A" -> 1
// prendo quello attivo (get active sheet)
var ss = SpreadsheetApp.getActiveSpreadsheet();
var DATA_SHEET = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
ss.toast("removing duplicates...", "", -1);
var dataCopy1 = DATA_SHEET.getDataRange().getValues();
var deleted_rows = 0;
if (dataCopy1.length > 0) {
var i = DATA_SHEET.getLastRow(); // start at bottom
while (i > 0) {
if (dataCopy1[i-1][FIRST_COLUMN-1] === value_to_check) {
ss.deleteRow(i);
deleted_rows++;
}
i--;
}
}
ss.toast("Done! " + deleted_rows + ' rows removed', "", 5);
}
You need to ensure that the index for deletion is the correct index. When you delete a row all bellow rows has their indexes changed -1.
So try this code:
if (dataCopy1.length > 0) {
var i = 1;
while (i < DATA_SHEET.getMaxRows() - deleted_rows) {
if ((dataCopy1[i][FIRST_COLUMN]).search(value_to_check) != -1) {
ss.deleteRow(i - deleted_rows);
deleted_rows++;
}
i++;
}
}