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.
Related
I am copying data from a spreadsheet titled after the specific month and placing it in my main spreadsheet. I have successfully copied the data into range K80:K94 on my Daily Hub sheet.
In range K80:K94 I now want to add a checkbox in column M if there is a value in column K. For example if there is a value in K80 and K81 there would be a checkbox in M80 and M81. I feel like this should be fairly straightforward, however I have tried a few different options including using IsBlank() and nothing seems to be working.
function dailyhubhabits() {
var montha = new Array(12);
montha[0] = "JANUARY";
montha[1] = "FEBRUARY";
montha[2] = "MARCH";
montha[3] = "APRIL";
montha[4] = "MAY";
montha[5] = "JUNE";
montha[6] = "JULY";
montha[7] = "AUGUST";
montha[8] = "SEPTEMBER";
montha[9] = "OCTOBER";
montha[10] = "NOVEMBER";
montha[11] = "DECEMBER";
var dailyhabitshubmonth = new Date();
var getdhmonth = montha[dailyhabitshubmonth.getMonth()];
Logger.log(getdhmonth);
var mhs = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(getdhmonth);
var monthhabitsogdata = mhs.getRange("C56:E70");
var gethabits = monthhabitsogdata.getValues();
Logger.log(gethabits);
var dhs = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("DAILY HUB");
var habitsarea = dhs.getRange("K80:K94");
monthhabitsogdata.copyTo(habitsarea);
//THIS IS WHERE I AM HAVING TROUBLE
var datavalues = dhs.getRange("K80:K94").getValues();
var data_leng = datavalues.length;
for(var i=0; i<data_leng; i++) {
if(datavalues[i][0].length != 0) {
dhs.getRange(i+1,14).insertCheckboxes();
}
}
}
You want to insert a checkbox on Column M when there is a value in the same row of column K.
There are two problems with this part of your script:
evaluating whether the cell has a value
defining the target range for the checkbox
Does the cell have a value?
length returns the number of records in an array, but it is not a good method for determining whether a cell contains a value. This is a popular topic; you might care to read Google Spreadheets Scripts: check if cell is empty for several methods.
a better approach is !== ""
Defining the target cell
dhs.getRange(i+1,14).insertCheckboxes(); - there are two problems here
Column M is 13
i starts at zero, so the first range value would be .getRange(1,14) = Cell N1.
so you need a variable that defines the startRow, such as:
var startRow = 80
REPLACE
//THIS IS WHERE I AM HAVING TROUBLE
var datavalues = dhs.getRange("K80:K94").getValues();
var data_leng = datavalues.length;
for(var i=0; i<data_leng; i++) {
if(datavalues[i][0].length != 0) {
dhs.getRange(i+1,14).insertCheckboxes();
}
}
WITH
var startRow = 80
var endRow = 94
var datavalues = dhs.getRange("K"+startRow+":K"+endRow).getValues()
var data_leng = datavalues.length;
for(var i=0; i<data_leng; i++) {
if(datavalues[i][0] !=="") {
dhs.getRange(i+startRow,13).insertCheckboxes()
}
}
SUGGESTION
In my understanding, here's your goal:
Check values in K80:K94
Insert a checkbox on a row in M that is adjacent to a row that isn't empty in the K80:K94 range.
Perhaps you could try this sample script to replace your current line on the section in inserting the check-boxes:
/** SUGGESTION
* 1. Iterate through the values in range K80:K94 & identify which aren't empty.
* 2. Get each non-empty values' row numbers.
* 3. To reduce runtime execution in the loop, if there are consecutive non-empty values, set them as a range (e.g. M80:M81). Otherwise a single value will be set as a single range (e.g. M83);
* 4. Iterate through these ranges & insert the checkboxes.
*/
var range = SpreadsheetApp.getActive().getRange('K80:K94');
var temp_values = range.getValues().map((x, i) => x != '' ? [x, (range.getLastRow() - (range.getNumRows() - i) + 1)].flat() : '*');
var ranges = temp_values.join().split('*').map(y => (y.replace(/[a-zA-Z,]+/g, '-')).split('-').filter(x => x != ''));
ranges.map(z => [...new Set([z[0], z[z.length - 1]])]).forEach(
row => row.length > 1 ? SpreadsheetApp.getActive().getRange(`M${row[0]}:M${row[1]}`).insertCheckboxes() :
SpreadsheetApp.getActive().getRange(`M${row[0]}`).insertCheckboxes()
);
/** End */
This sample script runs faster vs your current implementation as it shortens the data to be processed in the loop
Demo
Sample sheet
After running the script
I have a button that I want to click, which will scroll me to a certain position. I've done this in order to get me to row 100:
function ScrollMe(){
var file = SpreadsheetApp.getActiveSpreadsheet();
var sheet = file.getActiveSheet();
var row = 100;
file.setActiveCell(sheet.getRange(row,1));
}
What I want to do if a find a list of all cells that are in column 'B' that contain (REGEX=>"Version: [0-9]?[0-9]?[0-9][.]?[0-9]?[0-9]? [a-zA-Z]+"), and then go to the last value that this is like. So basically, go to the last cell in column 'B' that starts with "Version: " and then has a single, double, or triple-digit number, a decimal point, and then two numbers after, and then any amounts of letter text after the fact. I want it to look like this:
function ScrollMe(){
var file = SpreadsheetApp.getActiveSpreadsheet();
var sheet = file.getActiveSheet();
//C# lambda
var row = FindAll(a=> a.COLUMN == 'B' && a.VALUE.RegexMatch("Version: [0-9]?[0-9]?[0-9][.]?[0-9]?[0-9]? [a-zA-Z]+"));
file.setActiveCell(sheet.getRange(row,1));
}
I assume that you expect the script to find the last cell in the column B that match your regex. If that is the case, you can use this code:
function ScrollMe() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var data = sheet.getRange("B:B").getValues();
var regex = new RegExp(
'Version: [0-9]?[0-9]?[0-9][.]?[0-9]?[0-9]? [a-zA-Z]+');
for (var i = 0; i < data.length; i++) {
if (regex.test(data[i][0])) {
var lastMatch = i;
}
}
sheet.setActiveRange(sheet.getRange(lastMatch + 1, 2));
}
The previous code used your approach. It will first read the full column B, and after that will iterate to find the last cell that match the regex; and when it finds the cell, it will select it. Please, ask me if you have any doubts about the function.
I have a spreadsheet for soccer with an overview sheet (it's the first sheet) and 34 more sheets (for every matchday, called "x. ST") filled with values. In the overview sheet I want to get the sum of specific values from a set number of matchdays. I tried to realize this by an App Script. var start is the value that defines the first matchday, var end the last matchday. I want it as dynamic as possible so I can get values i.e. from Matchday 10 to 20 or 20 to 25 and so on.
Every sheet from start to end has a value in a given cell. This value can be "3" (for win), "1" (for draw) or "0" (for loss). Because this function should display the number of matchdays with wins, every "3" counts as 1, every "1" and "0" as 0. Logically the sum of all 1's is the number of the won matchdays. Here's the code:
function win(cell) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
var sum = 0;
var end = ss.getActiveSheet().getRange(21, 2).getValue() + 1;
var start = ss.getActiveSheet().getRange(21, 3).getValue() + 1;
for (var i = start; i < end ; i++ ) {
var sheet = sheets[i];
var val = sheet.getRange(cell).getValue();
if (val == "3") {
val = 1;
} else {
val = 0;
}
sum += val;
}
return sum;
}
Now, my problem is that the code does work, but the sum is always a 0 (zero), regardless of the number of sheets with "3" in the given cell. I really don't see where's the problem. Anyone with an idea?
EDIT: Here's the Spreadsheet. I'm not sure it will help, but whatever:
https://docs.google.com/spreadsheets/d/1SoE4cJVAeeurmB7bvvGENEWvu2Mg1xvA3B3hTobBxW8/edit#gid=0
EDIT2: Ok guys, there were simple errors and the code works now. First I had to change cell in getRange(), and then I had to guarantee every value is an integer. There was also an error with start and end, causing the code searching in the wrong sheets. Because this sheets are currently without any values, there was always the sum of 0. Well, here's the current code:
function win(cell) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
var sum = 0;
var end = ss.getActiveSheet().getRange(21, 2).getValue() + 1;
var start = ss.getActiveSheet().getRange(21, 3).getValue();
for (var i = start; i < end ; i++ ) {
var sheet = sheets[i];
var val = sheet.getRange([cell]).getValue();
parseInt(val) * 1;
if (val == 3) {
val = 1;
} else {
val = 0;
}
sum += val;
}
return sum;
}
I cannot be sure without looking at your spreadsheet, but just from looking at your code, I am guessing that you have the "Start" and "end" cells mixed up. The way it is now, both cells are in the same row, but end comes before start, which is not what you seemed to describe above.
I am writing a script that would normalize the selected phone numbers in a Google spreadsheet:
/**
* Normalizes phone numbers to ###-###-#### format.
*/
function normalizePhoneNumbers() {
var activeSheet = SpreadsheetApp.getActiveSheet();
var activeRange = activeSheet.getActiveRange();
var selectedCells = activeRange.getValues();
var phoneNumber = "";
for (var i = 0; i < selectedCells.length; i++) {
for (var j = 0; j < selectedCells[i].length; j++) {
phoneNumber = selectedCells[i][j]; // for better readability
if (phoneNumber) {
phoneNumber = phoneNumber.replace(/[^\d]/g, ""); // remove all non-digit characters from phone number
} else {
continue;
}
if (phoneNumber.length === 10) {
phoneNumber = phoneNumber.slice(0, 3) + '-' + phoneNumber.slice(3, 6) + '-' + phoneNumber.slice(6, 10);
} else {
continue;
}
selectedCells[i][j] = phoneNumber;
}
}
activeRange.setValues(selectedCells);
};
This works when I select a row or a column or a block of cells, but only changes one of the items if I control-select cells scattered around the spreadsheet. Is it possible to change multiple cells in a non-contiguous selection? If so, what am I doing wrong?
What you want to do is not exactly possible. For instance, activerange.setValues() expects a single 2D array (like what's returned by Range.getValues()). In either case your function could only get a single rectangular array of data, not a disjoint set of cells, hen you call getValues().
However, if you can deal with making this a custom function which is invoked within a spreadsheet (using something like =NORMALIZEPHONENUMBERS) then you can use map() as demonstrated under Optimization in the custom functions documentation.
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]
})
});
}