This is a multi-part question: I'm at a loss to explain it succinctly in one clear statement. I'll try to clean it up after getting some feedback:
In Google Docs, I would like to search a different tab (in the same spreadsheet) to find a cell which contains specific data. The data may be moved around, making it impossible to use a static cell reference.
//if 'Sheet2' of my spreadsheet contains the following...
A B C D
1 - - - -
2 - foo bar 6
3 - - - -
//...then some magical function would return C2
=getCell( 'Sheet2', "bar" )
Now that we've got this cell, I want to get the values of the adjacent cells on the same row.
//this would return "foo":
=getLeft( getCell( 'Sheet2', "bar" ) )
//and this would return 6
=getRight( getCell( 'Sheet2', "bar" ) )
I was able to get a function working that satisfies the above task. However, it is PAINFULLY SLOW! I'm using the function in about 100+ places, so this makes the sheet timeout on calculation every time I change something.
Can anyone suggest how to get the same functionality, but with much better performance?
function getCell( sheetname, item, row_offset, default_string )
{
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetname);
var lastrow = sheet.getLastRow();
var lastcol = sheet.getLastColumn();
for( var c = 1; c <= lastcol; c++ )
{
for( var r = 1; r <= lastrow; r++ )
{
var range = sheet.getRange(r, c);
if( range.getValue() == item )
{
//item found! See if we can get the requested cell...
c = c + row_offset; //adjust column using given offset
if( c < 1 || c > lastcol )
return "E.offset";
else
return sheet.getRange(r,c).getValue();
}
}
}
return default_string;
}
Could you add a row and a column before your data range as follows:
A B C D E F G
1 "bar"
2 - - - -
3 - foo bar 6
4 - - - -
You have a cell where you have the cell content you are looking for, let's say in G1. In cell A2 you place the formula:
=COUNTIF(B2:E2,$G$1)
Which will check row A for the a match on the cell G1. Fill down the other cells in row A and cell A3 will show "1".
Use a similar formula for cell B1:
=COUNTIF(B2:B4,$G$1)
And fill across the row. Cell D1 will show a 1.
Then, use INDEX() with MATCH() to find the contents of the cells on either side:
Left:
=INDEX($B$2:$E$4,MATCH(1,$A$2:$A$4,0),MATCH(1,$B$1:$E$1,0)-1)
Right:
=INDEX($B$2:$E$4,MATCH(1,$A$2:$A$4,0),MATCH(1,$B$1:$E$1,0)+1)
No scripting required!
Related
I have something like this on a sheet
NAME
POINTS
ELIGIBLE
FINAL
Alice
700
YES
Bob
500
NO
Carol
300
NO
Dave
200
YES
Eve
100
YES
I need to achieve the following in column D:
If it's a "NO", the final points will be the same as the original (like a placeholder) i.e. =B{row}
If it's a "YES", the final points will follow the same sequence as column B, except skipping all the "NO" in the list
so in this example, the output should be like this
NAME
POINTS
ELIGIBLE
FINAL
Alice
700
YES
700
Bob
500
NO
500
Carol
300
NO
300
Dave
200
YES
500
Eve
100
YES
300
When doing this manually, I just copy all values of B2:B, paste to D2, then apply filter "YES" to column C and paste to D2 once again. (this will paste some ugly trailing values to empty rows, but doesn't matter)
The current script
However, when doing this with script, I have to use a for loop to check column C of every row, then assign a =B{n} formula to column D, where n increases only when the current row is a YES
j = 2; //starting from the second row
for (var i = 0; i < lastRow - 1; i++) {
if (sheet.getRange(i+2,3).getValue() == "YES") {
sheet.getRange(i+2,4).setFormula(`B${j}`);
j++;
}
else {
sheet.getRange(i+2,4).setFormula(`B${j}`);
}
}
but this also means the processing time will scale with number of rows (10,000 - 20,000ms per ~100 row), all other actions I am doing only takes ~1000ms in total, which is why I'm looking for an option that does not need iterating to save a lot of time.
I don't mind not using formula in column D as long as the value is correct.
I have tried these
get B2:B as range, set filter (C1:C text equals to "YES"), then copyTo() D2:
seq = lbSheet.getRange(`B2:B${lastRow}`);
sheet.getRange("C1:C").createFilter();
filter = lbSheet.getFilter();
criteria = SpreadsheetApp.newFilterCriteria().whenTextEqualTo("YES").build();
filter.setColumnFilterCriteria(3,criteria);
seq.copyTo(sheet.getRange(`D2`),SpreadsheetApp.CopyPasteType.PASTE_VALUES,false);
this does not paste to filtered cells only, but all cells
getValues(B2:B), set filter (C1:C text equals to "YES"), getRange(D2:D).setValues(B2:B):
seq = lbSheet.getRange(`B2:B${lastRow}`).getValues();
sheet.getRange("C1:C").createFilter();
filter = lbSheet.getFilter();
criteria = SpreadsheetApp.newFilterCriteria().whenTextEqualTo("YES").build();
filter.setColumnFilterCriteria(3,criteria);
sheet.getRange(`D2:D${lastRow}`).setValues(seq);
this gives range mismatch error
The problem with the OP script is that the target formula is always B${j}. But this is only true then the value is "Yes".
When the value is "No", the target formula references the same row; i.e. B${i+2}.
Lastly, the value results are pushed onto a temporary array, and setValues is only used once at the end of the script.
function myFunction() {
var sheet = SpreadsheetApp.getActiveSheet()
var lastRow = sheet.getLastRow()
var tempArray = new Array
j = 2; //starting from the second row
for (var i = 0; i < lastRow - 1; i++) {
if (sheet.getRange(i+2,3).getValue() == "YES") {
// Logger.log("DEBUG: YES: i:"+i+", j:"+j+", range:"+sheet.getRange(i+2,3).getA1Notation()+", value = "+sheet.getRange(i+2,3).getValue()+", value range = "+sheet.getRange(i+2,4).getA1Notation()+" set value "+`B${j}`+" set value "+sheet.getRange(j,2).getValue())
tempArray.push([sheet.getRange(j,2).getValue()])
j++;
}
else {
// Logger.log("DEBUG: NO: i:"+i+", j:"+j+", range:"+sheet.getRange(i+2,3).getA1Notation()+", value = "+sheet.getRange(i+2,3).getValue()+", value range = "+sheet.getRange(i+2,4).getA1Notation()+" set value "+`B${i+2}`+" set value "+sheet.getRange(i+2,2).getValue())
tempArray.push([sheet.getRange(i+2,2).getValue()])
}
}
// update array values to column D
sheet.getRange(2,4,lastRow-1,1).setValues(tempArray)
}
My sheet consists of details of working hours of crew in shifts. Column A is serial no. Column E is total duty hours. One day duty consists of smaller shifts and some details like S.No, Name, crew id gets repeated.
Initial data
I want to merge column with same cell values (Column A & Column E). I have been able to merge Column A of S.No (thanks to #Tanaike from this Forum) and want to do same thing for Column E.
Achieved so far
What i want
Condition - If Column A is merged, exactly no of cells should merge in Column E. So, if A11, A12 are merged = E11, E12 should merge; A13 not merged = E13 not merged; A14, A15, A16, A17 are merged = E14, E15, E16, E17 should merge.
Thanks.
Relevant Code so far -
// merge columns vertically for same cell value for Column A
var start = 10; //data starts from row 10
var c = {};
var k = "";
var offset = 0;
// Retrieve values of column A
var data = destSheet.getRange(start, 1, lastRow-2, 1).getValues().filter(String);
// Retrieve the number of duplication values.
data.forEach(function(e){c[e[0]] = c[e[0]] ? c[e[0]] + 1 : 1;});
// Merge cells.
data.forEach(function(e){
if (k != e[0]) {
destSheet.getRange(start + offset, 1, c[e[0]], 1).merge();
offset += c[e[0]];
}
k = e[0];
});
Two approaches to solve your issue
Apps Script offers methods like isPartOfMerge() and mergeVertically() which would allow you to transfer the merge formatting from column A to column E.
You can do it by looping through all merged ranges in column A, retreiving their start and end row, and merge the respective ranges in column E:
var range = destSheet.getRange(1,1, destSheet.getLastRow(), 1);
var mergedRanges = range.getMergedRanges();
for (var i = 0; i < mergedRanges.length; i++) {
Logger.log(mergedRanges[i].getA1Notation());
var start = mergedRanges[i].getRow();
var end = mergedRanges[i].getLastRow();
var destinationRange = destSheet.getRange(start, 5, end - start + 1, 1);
destinationRange.mergeVertically();
}
You can copy the formatting from the entire column A to column E - this will copy the merging
To do so, you can use the method copyTo(destination, copyPasteType, transposed) specifying CopyPasteType as PASTE_FORMAT:
var range = destSheet.getRange(1,1, destSheet.getLastRow(), 1);
var destinationRange = destSheet.getRange("E1");
range.copyTo(destinationRange, SpreadsheetApp.CopyPasteType.PASTE_FORMAT, false);
Im am trying to match 2 columns (A:B) in sheet1 with 2 columns in sheet2 (A:B) and if there is a match, copy contents of column C matching row in sheet1 to matching row in sheet2.
I've tried to adapt several scripts without success. The code below comes closest to my requirements, but with my limited knowledge of script I haven't been able to adapt it to my exact needs.
Sheet1
A B C
Week Rotation Working
Week1 11 In
Week1 5 In
Week1 4 In
Week1 3 In
Week1 3 Off
Week1 7 Off
Sheet2
A B C
Week Rotation Working
Week1 6
Week1 5
Week1 4
Week1 3
Week1 3
Week1 11 (In should be copied to here)
My code:
function MatchColumns(){
// gets spreadsheet A and the range of data
var sheetA
=SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Testa");
var dataA = sheetA.getRange(2, 1, sheetA.getLastRow(),
2).getValues();
// gets spreadsheet B and the range of data
var sheetB =
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Test2");
var dataB = sheetB.getRange(2, 1, sheetB.getLastRow(),
1).getValues();
// Added
var res = [];
for (var b in dataB) {
for (var a in dataA) {
if (dataA[a][0] == dataB[b][0]) res.push([dataA[a][3]]);
}
if (b != res.length - 1) res.push([""]);
}
sheetB.getRange(2, 2, res.length, res[0].length).setValues(res);
Note that JavaScript is one of the many languages that use 0-base indexing. So res.push([dataA[a][3]]) is placing the 4th value from the row into the result array, i.e. Column D.
Your dataA and dataB variables don't actually include the column C data, as you initialized them with only 2 columns of data. So dataA[a][2] and dataA[a][3] are both undefined.
You probably don't want to collect these new values into an array via push, as this will lose the correlation between which row you matched in A & B, and which row you write into. To avoid losing existing information in column C, you need to read it from sheet 2 and assign to the specific index:
var destC = sheet2.getRange(2, 3, sheet2.getLastRow() - 1, 1).getValues();
/** Find matched rows */
...
destC[b][0] = dataA[a][2];
...
// Lastly, write values
sheet2.getRange(2, 3, destC.length, 1).setValues(destC);
Try using a nested for to iterate through the Values in Sheet1 and Sheet2 and compare them with
function MatchColumns(){
// gets spreadsheet A and the range of data
var sheetA = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
var dataA = sheetA.getRange(2, 1, sheetA.getLastRow() - 1, 2).getValues();
// gets spreadsheet B and the range of data
var sheetB = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet2");
var dataB = sheetB.getRange(2, 1, sheetB.getLastRow() - 1, 2).getValues();
for (var x = 0; x < sheetA.getLastRow() - 1; x++){
for (var y = 0; y < sheetB.getLastRow() - 1; y++){
if (dataA[x][0] == dataB[y][0]){
if (dataA[x][1] == dataB[y][1]){
sheetB.getRange(y + 2, 3).setValue(sheetA.getRange(x + 2, 3).getValue());
}
}
}
}
}
This copies the value of column C in Sheet1 to column C in Sheet2 if the corresponding cells in columns and B match.
I'm trying to write a script to paste rows of cells from one sheet to another while ignoring rows with blank cells in Column A. The script would also swap the order of Columns B and C. Here is what I am trying to do:
INPUT Sheet....................................OUTPUT Sheet
A1= ID B1= Last Name C1 = MI A1= ID B1 = MI C1= Last Name
A2= 1 B2= Stewart C2= M................A2= 1 B2= M C2= Stewart
A3= B3= Smith C3= R................A3= 4 B3= V C3= Holland
A4= 4 B4= Holland C4= V................A4= 3 B4= B C4= Young
A5= 3 B5= Young C5= B
Here is what I have:
function removeEmptyRows() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Input");
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Output");
var data = sheet.getRange("A2:D").getValues();
var newData = new Array();
for(i in data){
var row = data[i];
var empty = false;
for(i in data){
if(row[0] == ""){
empty = true;
}
}
if(!empty){
newData.push(row);
}
sheets.getRange(2, 1, newData.length, newData[0].length).setValues(newData);
};}
I have not been able to figure out how to incorporate changing the order of columns.
I was not able to apply previous posts to this problem, although this one came closest.
I would really appreciate any help you can provide. Thank you!
Currently my function looks like this:
function myFunction(value1, value2, value3, value4, value5, value6)
Basically value1 is always within column A, but shifts between rows. value2-6 always going be 1 cell to the right of each other. I.e., value1=A1, which means value2=B1, value3=B3 etc. OR value3=A5, value2=B5 etc.
I basically just want my input to be the 1st column and my program knows the read the values of 2-6, like this:
myFunction(value1)
How can I achieve that?
I am guessing that value1 is not a cell range, as google built-in Spreadsheet Service already provides getRange(row, column) with getRange().getA1Notation() and others in the Range class.
Sounds like you want the script to search column A for argument value1 and return the found row data?
function getValueRow (value1) {
var sh = SpreadsheetApp.getActive().getSheets()[1];
// Get each cell value in column "A" and flatten for indexOf() use
var bound_col = sh.getRange(1, 1, sh.getLastRow()).getValues().join().split(",");
// Compare value1 to column "A" data to find its row
// `indexOf()` returns the index of the first match
var argument_row = bound_col.indexOf(value1);
if (argument_row != -1) {
// Get the row data of value1 from column "A" to column "F"
var row_data = sh.getRange((argument_row + 1), 1, 1, 6).getValues();
Logger.log("%s is in row %s", value1, (argument_row + 1));
Logger.log("The row_data is: %s", row_data);
} else {
Logger.log("Can not find %s in column A", value1);
}
}
The following should be quite clear and efficient. At least it is working.
Note that especially with a simple pattern like this (all the cells in a row ) you should first define the range, then pick the values at once in an array. Picking the values one cell at a time is noticeably slower.
function myFunction( value1 )
{
var cell1 = SpreadsheetApp.getActiveSheet().getRange(value1);
// myFunction("A1") is the same as following:
//cell1 = SpreadsheetApp.getActiveSheet().getRange("A1");
// Define the value here, if it is fixed.
//Otherwise in the function parameters
var columnCount = 6;
// Define the range to get the values from. Parameters:
// Starting row of the range : row of Cell1
// Starting column of the range : column of cell1
// Number of rows = 1
// And the number of columns we wanted
var values = SpreadsheetApp.getActiveSheet()
.getRange( cell1.getRow() , cell1.getColumn() , 1 , columnCount)
.getValues();
// var values == [[ value, value, value, value, value, value ]]
// so actually values[0] is what you requested, the values from A1 to F1
// Now if you want to, for example sum up the values
// You just iterate through the values[0]
var sum = Number(0);
for ( var i = 0; i < values[0].length; i++ )
{
var cellValue = values[0][i];
sum += Number(cellValue);
}
return sum;
}
Remember that when you call the function in the Spreadsheet cell, you cannot add the parameter by just clicking a cell, as it would result as this: =myFunction(A1)
- That way A1 adds uses the VALUE of cell A1 as parameter.
You need to add quotation marks when calling the function, like this:
=myFunction("A1")
-That way you use the cell as parameter.