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:
Related
Question Updated for Clarity
I have the below formula running as intended. However I would like to limit the results found in A2:C to 2 of each SKU found. the below table has A11134 which has 4 results, but I want to only see the first 2. so I would see (A11134 E01D1 48) & (A11134 F06C2 48)
function PreparePrepPivotTable() {
var spreadsheet = SpreadsheetApp.getActive();
var sheet = spreadsheet.getSheetByName('PrepSheet');
sheet.getRange('G2').activate().setFormula('=QUERY({\'LOCATIONS\'!$A$2:$C},"SELECT * WHERE Col1
MATCHES \'" & JOIN("|",FILTER(O2:O, NOT(ISBLANK(O2:O)))) & "\' ")');
}
Thanks In Advance
NEW ANSWER
Ok in this case I would suggest using a script to obtain as much as SKU you want.
First you use this function to filter the O coulmn and get your matching set. This set will contain a counter initially set to 0.
function GetNSKUS() {
var ss =SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('PrepSheet');
//Building the matching set.
var matchingValues = ss.getRange("O2:O").getValues().filter(value => value[0] != '').map(nonBlankValue => [nonBlankValue[0], 0]);
//Filtering out the desired number of values
var values = sheet.getRange("LOCATIONS!$A$2:$C").getValues().filter(value => advancedFilter(value[0], matchingValues));
let cols = values[0].length;
let rows = values.length;
//Printing out the found rows starting from G2
sheet.getRange(2, 7, rows, cols).setValues(values);
}
You will then filter your LOCATION Sheet range with this function. When a match is done the counter will increment allowing you to filter up to the desired values.
function advancedFilter(value, toMatch) {
let matched = toMatch.filter(couple => couple[0] === value);
if (matched.length > 0) {
if (matched[0][1]<2) { //Here you can decide when to stop returning SKUs
matched[0][1] += 1;
return true;
}
}
return false;
}
OLD ANSWER
You can use limit to reduce your query output. In your case if you want only to return the first 2 rows matching your query you can use the following:
function PreparePrepPivotTable() {
var spreadsheet = SpreadsheetApp.getActive();
var sheet = spreadsheet.getSheetByName('PrepSheet');
sheet.getRange('G2').activate().setFormula('=QUERY({\'LOCATIONS\'!$A$2:$C},"SELECT * WHERE Col1 MATCHES \'" & JOIN("|",FILTER(O2:O, NOT(ISBLANK(O2:O)))) & "\' LIMIT 2")');
}
References:
Limit
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 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'm using google sheets and have never run a "cron job" here.
To give credit where it's due, I found this awesome code to help me access the speed insights api here: https://statsravingmad.com/measure/page-speed-insights/
function speed(url,device,filter_third_party_resources,http_secure) {
url = url || 'www.statsravingmad.com';
strategy = 'desktop' || device;
filter_third_party_resources = 'true' || filter_third_party_resources;
http_secure = 'false' || http_secure ;
switch (http_secure) {
case 'false':
http_protocol = 'http://';
break;
case 'true':
http_protocol = 'https://';
break;
}
var key = 'api-key';
var api = 'https://www.googleapis.com/pagespeedonline/v2/runPagespeed?url=' + http_protocol + url
+ '&filter_third_party_resources=' + filter_third_party_resources +
'&strategy=' + strategy + '&key=' + key;
var response = UrlFetchApp.fetch(api, {muteHttpExceptions: true });
var result = JSON.parse(response.getContentText());
score = result.ruleGroups.SPEED.score;
return(score);
}
So I have this code in a function that is triggered every hour for my particular test sites in my google sheet.
But, the data is only filling one cell per site, the cell that the formula is assigned to.
When using google sheets, how can I modify this in order to have it fill a new cell in a column every hour? Do I modify this code, do I have to set up another function, or is there an option to fill the cells down a column?
This function can be modified to write, say, to column C of Sheet1. Here is how it would end, instead of return(score) (there is no need to return anything if the value is written to the spreadsheet directly; the function would not be invoked from the spreadsheet, but from a trigger).
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
var values = sheet.getRange("C:C").getValues(); // using column C
var lastRow = 1;
for (var i = 0; i < values.length; i++) {
if (values[i][0] != "") {
lastRow = i + 1;
}
}
sheet.getRange(lastRow + 1, 3).setValue(score); // column = 3 because C
Here the loop finds the last row in column C that has data, and the values of score is placed under it.
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]
})
});
}