GAS: Finding the last column in a given row - google-apps-script

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.

Related

Google Sheets - How can I find the first row number containing specific text, and then clear/delete all content to end?

I'm currently importing a bunch of data and then splitting it into multiple columns and then attempting to clear or delete any erroneous rows (it's a raw data import that contains a bunch of scrap rows).
So far I've got the data imported, split, & sorted. Now I'm trying to find the row number, based on a value in column A, and then select all rows to the end of the sheet to either delete or clear the content.
I'm to the point where all of my data has been split into the columns I need (A:J) and sorted so that all relevant data is at the top (it's a variable data set) so now I'm just trying to find the first row that contains "----------------------" as this will be my first 'garbage' row.
outputrange.setValues(output);
pull1.deleteRows(1, 40);
pull1.getRange(2, 1, pull1.getLastRow()-1,
pull1.getLastColumn()).activate().sort({column:2, ascending: true});
var removalValues = range.getValues()
for (var j=0; j<removalValues.length; j++) {
var rowArray = removalValues[j];
for (var k=0; k<rowArray.length; k++) {
var columnValue = rowArray[k];
if (rowArray[0] === "----------------------") {
var rowNumber = i;
pull1.getRange(rowNumber, 1, 1, pull1.getLastColumn()).activate()
}
}
}
I've attempted the code above to loop through and find the correct cell reference, and just temporarily highlight the row so I make sure it's functioning correctly. Currently this part of my code processes but otherwise doesn't do anything. Really I just need something that will look through my data in column A and find the matching data, then return the row number for me so that I can apply it to other formulas.
Edit: I updated my code using some additional resources and came up with the following. It seems to work correctly but I'm not sure if it's the most efficient solution:
var outputrange = pull1.getRange(startRow, 1, LR-startRow+1, 10)
outputrange.setValues(output);
pull1.deleteRows(1, 40);
pull1.getRange(2, 1, pull1.getLastRow()-1,
pull1.getLastColumn()).activate().sort({column:2, ascending: true});
var rangeData = pull1.getDataRange();
var lastColumn = rangeData.getLastColumn();
var lastRow = rangeData.getLastRow();
var searchRange = pull1.getRange(1,1,lastRow-1,lastColumn-1);
var removalValues = searchRange.getValues();
for (j=0; j < lastColumn-1; j++) {
for (k=0; k < lastRow-1; k++) {
if(removalValues[k][j] === "----------------------") {
pull1.getRange(k+1, 1, pull1.getLastRow(), 10).deleteCells(SpreadsheetApp.Dimension.ROWS);
}
}
}
Requirement:
Find specified text then delete all rows below said text.
Solution:
Use textFinder to find the text then pass the result row to deleteRows().
function findAndClear() {
var sh = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var lastRow = sh.getLastRow() + 1;
var range = sh.getRange(1, 1, lastRow);
//find specified text and return row number
var row = range.createTextFinder('----------------------').findNext().getRow();
//calculate number of rows to delete
var delRows = lastRow - row;
//delete rows
sh.deleteRows(row, delRows);
}
Explanation:
First of all, sorry but I've scrapped the script you were using. There's no need to get all of the values in the column then loop through each one, it's over-complicated and slow.
Using textFinder is much quicker, no looping through arrays, just finding the first occurrence of your string in column A and getting the row number.
Notes:
If you did want to keep the row containing your string, use:
sh.deleteRows(row + 1, delRows);
References:
textFinder Documentation

How to get Row where two Columns equal specific values

I'm very new and I'm trying to create a Time in/Time out sheet. I have 2 separate sheets, first(ACTIVE) is where the trigger happens that starts the onEdit(e) script. All the functions that start onEdit(e) affects the second sheet(PASSIVE) to fill out Columns A(Last Name), B(First Name), C(Location), D(Time Out). I finished making the Time out functions by getting value of A, B, C + Active Row(this isn't the code). The trigger is always on the same row as the values being copied, so it was relatively simple. On the PASSIVE sheet I have all the values being stored using a code someone made called addRecord where it gets last row + 1 of the PASSIVE sheet and installs the values grabbed from the ACTIVE sheet and plugs them in. So it adds records without overwriting anything. Works beautifully. However making a "time in" function has been difficult. E(Time In) My idea is to getRow of the PASSIVE sheet by searching PASSIVE!A for the Value grabbed from (ACTIVE!A + Active Row) once it finds a match, it sees if (PASSIVE!E + the matched row) is empty. If it is, it adds new.Date and finishes. If it isn't empty, it ignores this row and continues searching down the line for the next Row that has PASSIVE!A match the grabbed value. Once it finds this Row, getRow. setValue of (PASSIVE!E + grabbed row, new Date())
I did find a function online to find the first row that matched the ACTIVE!A with PASSIVE!A. But it kept overwriting the date on the first match. It never ignored row with nonempty cell to the next match row. Maybe I was just slightly off, which is why I'm asking for a lot of detail and explanation in the Answers.
This was the Code I used from another answer.
function getCurrentRow() {
var currentRow = SpreadsheetApp.getActiveSheet().getActiveSelection().getRowIndex();
return currentRow;
}
function onSearch1()
{
What I added
var row = getCurrentRow();
var activeLocation = getValue('ACTIVE!A' + row);
Continued Other Code
var searchString = activeLocation;
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("PASSIVE");
var column =1; //column Index
var columnValues = sheet.getRange(2, column, sheet.getLastRow()).getValues(); //1st is header row
var searchResult = columnValues.findIndex(searchString); //Row Index - 2
What I added
setValue(PASSIVE!E + searchResult, new Date().toLocaleString())
It worked if everyone has a different name, but the search Result always found the first row of the match, I tried adding an if ACTIVE!A == PASSIVE!A
&& PASSIVE!E =="", grabRow (I know this isn't proper code) But I didn't even know where to put this if function or if it would work or if it would just keep coming up false after it runs the first time true.
Continued Other Code
if(searchResult != -1)
{
//searchResult + 2 is row index.
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("PASSIVE").setActiveRange(sheet.getRange(searchResult + 2, 1))
}
if(searchResult = searchResult2) {
setValue('PASSIVE!E' + searchResult, new Date().toLocaleString())
}
}
Array.prototype.findIndex = function(search){
if(search == "") return false;
for (var i=0; i<this.length; i++)
if(this[i] == search) return i;
return -1;
}
So this is what I used, but not sure if it's the right way to go about this. Every time I used it, it would only set the SearchResult to the first row it found that had the searchString I'd actually prefer if it found the last row, considering the add record goes down over time and signing in should be the most recent name. But I'm guessing if I can just get a function that searches a range and finds the row for two values in specific columns, I can then just setValue('PASSIVE!E' + foundRow, new Date().toLocaleString())
Edit 5/9/2019 17:34 PST
Thank you to those Answering. I'm expanding on the question.
function rowWhereTwoColumnsEqual(value1,col1,value2,col2) {
var value1=value1 || 'A1';//testing
var value2=value2 || "";
The idea I'm having is to search Column1 of another sheet for, let's say, 'SheetA1' (the first sheet). And Column3 of another sheet for "" (cellisempty).
var value1= 'Sheet1!A1';
var value2= "";
var col1='Sheet2!A';
var col2='Sheet2!C';
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName('Sheet2');
var rg=sh.getDataRange();
var vA=rg.getValues();
However, I don't know how the vA works. I also want to getRow() of the Row that is found in order to use that number in another function.
Try this:
function rowWhereTwoColumnsEqual(value1,col1,value2,col2) {
var value1=value1 || 9;//testing
var value2=value2 || 8;
var col1=col1 || 1;//testing
var col2=col2 || 2;//testing
var ss=SpreadsheetApp.getActive();
var sh=ss.getActiveSheet();
var rg=sh.getDataRange();
var vA=rg.getValues();
var rA=[];
for(var i=0;i<vA.length;i++) {
if(vA[i][col1-1]==value1 && vA[i][col2-1]==value2) {
rA.push(i+1);
}
}
SpreadsheetApp.getUi().alert(rA.join(','));
//return rA;//as an array
//return rA.join(',');//as a string
}

Google Sheets: INDIRECT() with a Range

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
}
}
}
}

custom spreadsheet function - get a1 notation for the range?

I'm trying to create a custom function for a google sheet that will find the rightmost string in a 1d range of cells, then return a header (in a specified row).
Here's where I'm stuck. I can get the string for that cell with the following code:
function FarRightHeader(range, rownumber) {
var cells = range[0].length;//gets the number of cells
for (var i = 0; i < cells; i++) { //loop through the cells in the range
var j = cells - 1 - i; // j will start at the end so the loop can work from left to right
if (range[0][j] != "") { //if the cell contains something
break; //jump out of the loop
}
}
var activeCell = SpreadsheetApp.getActiveRange().getA1Notation();
var activeColumn = activeCell.charAt(0);
var FarRightCell = "Hi, I'm___" + range[0][j] + "___ and I'm in column " + activeColumn;
return FarRightCell;
}
here's the glitch - the activeCell variable is taking the cell from which the custom function is called, not the far right populated cell in the range. I understand why this is happening, but don't know how to get the column I want.
To me it appears that the function is treating the range as simply the values in the cells divorced from what cells they actually are in the spreadsheet.
Is there a way to get information about the range within the spreadsheet that the function takes as a parameter?
Thanks in advance for any help or leads you can give me!
I see no glitch, you're imagining your loop as searching the cells, while you're just searching an Array that you got from the cells values.
But as your code sugests you don't need to retrieve the column like, you already have it, saved in the j, you just need to convert it to a letter, here's a code I fetched:
function colName(n) {
var ordA = 'a'.charCodeAt(0);
var ordZ = 'z'.charCodeAt(0);
var len = ordZ - ordA + 1;
var s = "";
while(n >= 0) {
s = String.fromCharCode(n % len + ordA) + s;
n = Math.floor(n / len) - 1;
}
return s;
}
Also here's a suggested better for loop:
var column; // Initialize before so if doesn't become private scope
for (column = ( range[0].length - 1 ); column > 0; column--) {
if (range[0][j] != "")
break;
}
Note: This requires that the Range starts at "A" column, if it doesn't you have to add the range first column to column.

Non-contiguous column copy from one spreadsheet to another in google apps

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]
})
});
}