I've been having a hard time trying to figure this out. I realize this is perhaps more basic than usual for those who follow the GAS tag, however any help much appreciated.
If I'm breaking up my bigger task into component parts, my goal right now with this question is to update several named ranges automatically.
There is a tab on the spreadsheet called "DataImport". DataImport has 10 columns all 1000 rows long. There is a named range for each column e.g. cabbages (A2:A1000), dogs (B2:B1000) etc etc.
There is a script attached to a new menu item "Update Data" that when selected imports a csv file into DataImport tab meaning that the length of the data set will grow.
How can I tell the sheet to update each named range to be the length of data? So if the original named range "cabbages" was A2:A1000 and following an update the data now actually goes as long as A2:A1500, how would I tell the sheet to update the range cabbages?
I found a snippet of code online and started to fiddle with it:
function testNamedRange() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var range = ss.getRange('DataImport!A:A');
var data_len = range.length;
SpreadsheetApp.getUi().alert(data_len); // "alert just gives "undefined"
ss.setNamedRange('TestRange', range);
var rangeCheck = ss.getRangeByName('TestRange');
var rangeCheckName = rangeCheck.getA1Notation();
}
My thinking was if I could just get the length of data following an update using the custom menu function, I could then use setNamedRange() to update cabbages range.
I'm really lost and I imagine this is simpler than I'm making it out to be.
How can I update the named range cabbages to be the length of data in UpdateData column A?
Edit: IMPORTANT
Use INDIRECT("rangeName") in formulas instead of just rangeName.
The only way to extend the range programmatically is by removing it and then adding it back with a new definition. This process breaks the formula and returns #ref instead of the range name. This should be an unnecessary work around. if you agree please star and the issue tracker at: https://code.google.com/p/google-apps-script-issues/issues/detail?id=5048
=sum(indirect("test1"),indirect("test3"))
Emulates open ended named ranges by checking to see that the last row in the named range is the same as the last row in the sheet. If not, adjusts the named range so the last row in the named range is the same as the last row in the sheet.
should probably be used with on open and on change events.
function updateOpenEndedNamedRanges() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// names of open-ended ranges
var openEndedRangeNames = ["test1", "test2", "test3", "s2test1" ];
for(i in openEndedRangeNames) {
var rName = openEndedRangeNames[i];
try{
var r = ss.getRangeByName(rName);
}
catch(err) {
GmailApp.sendEmail("me#gmail.com",
rName + " -- Cannot find",
"Trying to update open-ended ranges after rows added. \n"
+ "Unable to find range name-- "+ rName
+ " -- in ss ( " + ss.getName() + " ) "
+ "\n If it is not needed please remove it "
+ "from array \n openEndedRangeNames[] \n in the function \n"
+ "updateOpenEndedNamedRanges()");
continue;
}
var rlr = r.getLastRow();
var s = r.getSheet();
var slr = s.getMaxRows();
if(rlr==slr ) continue;
var rfr = r.getRow();
var rfc = r.getColumn();
var rnc = r.getNumColumns();
var rnr = slr - rfr + 1;
ss.removeNamedRange(rName);
ss.setNamedRange( rName, s.getRange(rfr, rfc, rnr, rnc ));
}
}
function ssChangeEvent(change) {
// changeType (EDIT, INSERT_ROW, INSERT_COLUMN, REMOVE_ROW,
// REMOVE_COLUMN, INSERT_GRID, REMOVE_GRID, or OTHER)
switch(change.changeType) {
case "INSERT_ROW":
updateOpenEndedNamedRanges();
break;
default:
Logger.log(change.changeType + " detected. No action taken ");
}
}
Setup ssChangeEvent(change) to run when rows are added
Resources>this projects triggers
Offering this function I wrote to handle dynamic resize of named ranges:
function resizeNamedRange(rangeName, addRows, addCols) {
/* expands (or decreases) a range of a named range.
rows and columns to add can be negative (to decrease range of name). Params:
rangeName - name of range to resize.
addRows - number of rows to add (subtract) from current range.
addCols - number of columns to add (subtract) from current range.
Call example: resizeNamedRange("Products",1,0);
*/
var sh = SpreadsheetApp.getActiveSpreadsheet();
try {
var oldRange = sh.getRangeByName(rangeName);
var numRows = oldRange.getNumRows() + addRows;
var numCols = oldRange.getNumColumns() + addCols;
if (numRows < 1 || numCols <1) {
Logger.log("Can't resize a named range: minimum range size is 1x1.");
return;
}
sh.setNamedRange(rangeName, oldRange.offset(0,0,numRows, numCols));
} catch (e) {
Logger.log ("Failed resizing named range: %s. Make sure range name exists.", rangeName);
}
}
Maybe I'm missing something, but the function below takes a rangename and the range that it should contain. If the rangename already exists it updates the range to the passed value. If the rangename doesn't exist, it creates it with the passed range values.
Also with regard to the "#REF!" problem in the sheet. You can do a find and replace and tick the box for "find in formulas". Put "#REF!" in find and the named range name in the replace box. This assumes only one named range was deleted and that there were no other unrelated #REF! errors. This approach helped me fix a spreadsheet with the error spread over 8 different sheets and 20+ formulas in just a few minutes.
/**
* Corrects a named range to reflect the passed range or creates it if it doesn't exist.
*
* #param {string} String name of the Named Range
* #param {range} Range (not string, but range type from Sheet class)
* #return {void || range} returns void if Named Range had to be created, returns NamedRange class if just updated. This needs improvement.
* #customfunction
*/
function fixNamedRange (name, range) {
var i = 0;
var ss = SpreadsheetApp
.getActiveSpreadsheet();
var ssNamedRanges = ss.getNamedRanges();
for (i = 0; i<ssNamedRanges.length && ssNamedRanges[i].getName() != name; i++) {};
if (i == ssNamedRanges.length) {
return (ss.setNamedRange(name, range));
} else {
return (ssNamedRanges[i].setRange(range));
}
}
I found the solution!
I have a cell with a drop down list with all the clients that the company has registered on the system, if the name we enter does not appear on the list, then function newClient executes. Basically we use the SpreadsheetApp.getUi() in order to save the new information. Once we have introduced the client data, function creates a new row on the client's sheet and enters the information from the prompts on the last row. Once done, updates the drop down list automatically.
The real function is inside of a big function that calls newClient if it's needed so the real one would be newClient(client, clients), on the example I put the variables in order to make it easier.
I hope it works!
function newClient() {
var ss = SpreadsheetApp.getActive().getSheetByName('Clients'); // Sheet with all the client information, name, city, country...
var client = 'New company';
var clients = ss.getRange('A2:A').getValues();
var ui = SpreadsheetApp.getUi();
ui.alert('Client '+client+' does not exist, enter the next information.');
var city = ui.prompt('Enter city').getResponseText();
var country = ui.prompt('Enter country').getResponseText();
client = client.toUpperCase();
city = city.toUpperCase();
country = country.toUpperCase();
ui.alert('Here is the information you entered about '+client+':'+'\n\n'+'City: '+city+'\n\n'+'Country: '+country)
ss.insertRowAfter(ss.getLastRow()); // Insert a row after the last client
ss.getRange('A'+(clients.length+2)).setValue(client); // Let's suppose we have 150 clients, on the first row we have the titles Client, City, Country, then we have the 150 clients so the last client is on row 151, that's why we enter the new one on the 152
ss.getRange('B'+(clients.length+2)).setValue(city);
ss.getRange('C'+(clients.length+2)).setValue(country);
var namedRanges = SpreadsheetApp.getActive().getNamedRanges(); // We get all the named ranges in an array
for (var i = 0; i < namedRanges.length; i++) {
var name = namedRanges[0].getName();
if (name == 'Clients') { // All the clients are stored on 'Clients' named range
var range = ss.getRange('A2:A'); // Update the range of the 'Clients' named range
namedRanges[i].setRange(range);
}
}
ui.alert('Client created, you can find it on the drop down list.');
}
Related
The script, that I will post below, allows me to use a check box to move an entire row from one sheet to another in the same workbook. Everything works (I was following a YouTube tutorial), but it moves the entire row of data. Instead, I need it to move only data from specific columns, i.e. move the info from column A to C or move the info from column A and column K. I know this might be a simple question, but I just don't know which line to modify and how to modify it so that the script only pulls the info from specific columns within the row to move to the other sheet.
function sendMail() {
var client = 3;
var date = 0;
var email = 19;
var dnr = 18;
var emailTemp = HtmlService.createTemplateFromFile("email");
var ws = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Promos");
var data = ws.getRange("A3:T" + ws.getLastRow()).getValues();
data = data.filter(function(r){return r[1] == true })
data.forEach(function(row){
emailTemp.client = row[client];
emailTemp.date = row[date];
emailTemp.dnr = row[dnr];
var htmlMessage = emailTemp.evaluate().getContent();
GmailApp.sendEmail(row[email],
"Promo Reminder",
"Your email does not support html",
{name: "JV Ops", htmlBody: htmlMessage}
);
});
}
I tried to modify the line "var row = range.getRow()" by putting column letters in the parenthesis but it didn't do anything, not only did it not move the info from those specific columns but it no longer moved the entire row. It just no longer did anything.
You have a spreadsheet in which a "checked" checkbox identified the row(s) that are to be sent an email. However you can't identify the actual row number of the checked checkbox.
There are two issues in your script:
data = data.filter(function(r){return r[1] == true }) replaces the array that contains ALL the data on the sheet with just those rows that contain checked checkboxes.
you need to compare the new filtered array with the original array in order to get the row numbers. I adapted the method described in StackOverflow compare rows on google spreadsheets
the array equalRows contains the row numbers/indexes that contain checked checkboxes.
the OP can use the row number together with the mailData array to copy data on the relevant row.
function myFunction() {
var ws = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Promos")
var data = ws.getRange("A3:T" + ws.getLastRow()).getValues();
// Logger.log(data) // DEBUG
var mailData = data.filter(function(r){return r[1] == true })
// Logger.log(mailData) // DEBUG
// compare rows on google spreadsheets
// https://stackoverflow.com/a/34960630/1330560
var equalRows = [];
for(var i in data){
var anEqualRow = false;
for(var j in mailData){
if(data[i].join() == mailData[j].join()){
anEqualRow = true;
}
}
if(anEqualRow){
var row = +i+3
equalRows.push(row);
}
}
// Logger.log(equalRows) // DEBUG
}
I would like a Function in the Script attached to the Spreadsheet, to iterate through all the Named Ranges, and enter them in cells in a "Data" sheet. This sheet has lots of lists etc, which are used by my main sheet ("Analysis").
Ideally, this would put them starting at Row 1001, with col A being the Name, and col D being the Range. (Cols B & C left blank to allow for Overflow from col A - don't want to resize col A)
To be effective, any existing data in the rows previously entered by the Function should first be deleted, before updating with the new data.
Although I'm getting the hang of the Script language, I'm struggling with this, so if anyone could provide an example function, it would be appreciated! MTIA.
[edit]
Apologies if my original question was inadequate. I had some code, but it was rubbish, I knew it, so I scrapped it. Almost there now (I think) but one more prob to sort, getting an exception:
function namedRangeToSpreadsheet() {
// Author: Cooper
// Link: https://stackoverflow.com/a/64841828/190925
// Purpose: Record all NamedRanges by both Name and Range, as a check that
// public copy is NOT missing any!
const ss=SpreadsheetApp.getActive();
const nrA=ss.getNamedRanges();
var startRow = 1001; // MH insert from this row
var endRow = 1200; // MH clear|insert to a max. of this
// var targetSh = "Data"; // MH revert to Data ss when tests OK
var targetSh = "Named Ranges";
// MH clear previous data
let sh=ss.getSheetByName(targetSh);
var range = sh.getRange(targetSh + "!A" + startRow + ":D" + endRow);
range.clear();
//sh.getRange(sh.getLastRow()+1,1,rowA.length,4).clear();
let rowA=[];
for(let i=0;i<nrA.length;i++) {
let name=nrA[i].getName();
let range=nrA[i].getRange().getA1Notation();
rowA.push([name,'','',range]);
}
rowA.sort(); // MH
/* Exception: The number of rows in the data does not match the number of rows in the range.
The data has 32 but the range has 200. (line 348, file "Code") */
//sh.getRange(sh.getLastRow()+1,1,rowA.length,4).setValues(rowA);
range.setValues(rowA);
}
function namedRangeToSpreadsheet() {
const ss=SpreadsheetApp.getActive();
const nrA=ss.getNamedRanges();
let rowA=[];
for(let i=0;i<nrA.length;i++) {
let name=nrA[i].getName();
let range=nrA[i].getRange().getA1Notation();
rowA.push([name,'','',range]);
}
let sh=ss.getSheetByName('Sheet1');
sh.getRange(sh.getLastRow()+1,1,rowA.length,4).setValues(rowA);
}
I've been making slow but steady progress on this app that creates the daily bulletin for the school where I teach.
Data is submitted by staff via a form, and is then naturally in a sheet. I already created a script to purge old data from the sheet, thanks in part to help I've gotten here. An additional script orders content on the data sheet by bulletin category, creates a copy of a template sheet, names it by the desired date, puts the date at the top. That's about as far as I've gotten. It also adds the first category heading by default, which is mostly a test.
What I'm attempting to do now is loop through each row of the data sheet to determine if any of the three date columns contains the desired date (entered via a dialog box earlier in the script). If any of them match today's date, we then will check to see if the current category and the category in the row are the same. If they are not, we change the current category and add a new heading to the bulletin sheet. If they are the same, we get the announcement itself and add that to the bulletin sheet. I suspect I'll use embedded functions for these two purposes.
Right now I'm stuck on the loop portion. Again, this should cycle through each row of the data sheet. There are three columns containing the dates (C, D, E). If I can get it to recognize date matches from one of the cells in this range, I can move forward with the rest.
function writeBulletin() {
//get the bulletin date
var bullSheet = todayDay;
//make the bulletin sheet active
var ss = SpreadsheetApp.getActiveSpreadsheet();
ss.setActiveSheet(ss.getSheetByName(todayDate));
//set var for needed sheets
var responses = ss.getSheetByName("Form Responses 1")
var bulletin = ss.getSheetByName(todayDate)
//get the date from the sheet title and apply it to the date range
var dateCell = bulletin.getRange(3,1);
var sheetDate = bulletin.getName();
dateCell.setValue(sheetDate);
//works
//Now we start building the bulletin
//currentDataRow is a reference to the Responses sheet. Used in later for loop
var currentDataRow = 2;
var currentBulletinRow = 11;
var catCurrent = "01 Administration";
var catCurrentSS=catCurrent.substring(3,30);
var lastRow = responses.getLastRow(); //get last row of data sheet
var lastBull = bulletin.getLastRow(); //get last row of bulletin sheet
var nextBullRow = lastBull+2;
var testOutput = bulletin.getRange(6,3);
var nextBullItem = bulletin.getRange(nextBullRow,1);
nextBullItem.setValue(catCurrentSS);
//testOutput.setValue("dude"); //this works
if(responses.getRange(2,3).getValue()==todayDate) {
testOutput.setValue("dude");
}
//bulletin.getRange(2,3).setValue("dude"); //test row
for(var i = 2; i<=lastRow; i++) {
if(5>3) {
//if(responses.getRange(i,3).getValue()==sheetDate||responses.getRange(i,4).getValue()==sheetDate||responses.getRange(i,5).getValue()==sheetDate){
//bulletin.getRange(nextBullRow,3).setValue("dude");//works
bulletin.getRange(nextBullRow,1).setValue(responses.getRange(i,7).getValue());
nextBullRow+=2;
}
}
}
I did notice that my loop condition statement had a reversed inequality sign; however, fixing this did not seem to help.
jdv: Good point. fixed it now
Aside from the issue of repeatedly interacting with the Spreadsheet interface (the alternative being to read values from the Spreadsheet once, then work with the resulting javascript Array object), the issue is that you are comparing a Range object with a String:
var sheetDate = bulletin.getName();
...
if(responses.getRange(i, 3) == sheetDate || ..... ) {
This will not work :) You need to access the value of the Range:
if(responses.getRange(i, 3).getValue() == sheetDate || ... ) {
edit: as mentioned in comments, the values in these responses cells are interpreted as Date objects. Date comparisons are fun, because you get to play with time zones and/or format strings. I recommend avoiding needing to use dates in this manner, especially when starting out with scripts.
One possible fix for this new issue is to use the value from dateCell.getValue() after calling SpreadsheetApp.flush() (to ensure the writing of sheetDate is performed first). This will let the spreadsheet do the nasty work making the correct date:
dateCell.setValue(sheetDate);
SpreadsheetApp.flush();
// Construct the proper Date object from the sheetDate value
var compareDate = dateCell.getValue();
...
for(var i = 2; i <= lastRow; ++i) {
// Read into an array [[ 0: elem#(i,3), 1: elem#(i,4), 2: elem#(i,5), 3: elem#(i,6), 4: elem#(i,7) ]]
var row = responses.getRange(i, 3, 1, 5).getValues();
if(row[0][0] == compareDate || row[0][1] == compareDate || row[0][2] == compareDate) {
...
Let's say I have a lot of columns and one of them contains "impressions" string (on row 3). What I need to do is to:
1) Find the cell with "impressions" string
2) Get column number or i.e. "D"
3) Based on what I got paste a formula into i.e. D2 cell which gets AVERAGE from a range D4:D*last*
I couldn't find it anywhere so I have to ask here without any "sample" code, since I have no idea on how to achieve what I want. (3rd one is easy but I need to get that "D" first)
There's no way to search in Google Apps Script. Below is a function that will accomplish the first 2 parts for you (by iterating over every cell in row 3 and looking for "impressions"):
function findColumnNumber() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Sheet1'); // insert name of sheet here
var range = sheet.getDataRange(); // get the range representing the whole sheet
var width = range.getWidth();
// search every cell in row 3 from A3 to the last column
for (var i = 1; i <= width; i++) {
var data = range.getCell(3,i)
if (data == "impressions") {
return(i); // return the column number if we find it
}
}
return(-1); // return -1 if it doesn't exist
}
Hopefully this will allow you to accomplish what you need to do!
The indexOf method allows one to search for strings:
function findColumnNumber() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet() //whatever tab the code is run on
var data = sheet.getDataRange().getValues();
var header_row_num = 1; // TODO: change this to whichever row has the headers.
var header = data[header_row_num -1] //Remember JavaScript, like most programming languages starts counting (is indexed) at 0. For the value of header_row_num to work with a zero-index counting language like JavaScript, you need to subtract 1
//define the string you want to search for
var searchString = "impressions";
//find that string in the header and add 1 (since indexes start at zero)
var colNum = header.indexOf(searchString) + 1;
return(colNum);
Here is the set up
We have a contest with all employees based on project scores. Each project has two categories of employees(4 employees per category) and two scores(one for each category of employee).
I need to grab all the scores for the employees and output it into a spreadsheet. The following spreadsheet has misc. columns removed
Sheet Explanation
The sheet labeled "Example data" is the source we will be pulling data from
We need to match Editor and Editor Score
We need to match Webmaster and webmaster score
The sheet labeled "Example output" is what I want to be generated in another spreadsheet named "Contest Result" with the sheet name from the source sheet(They are named by date ranges).
We need to compile each employee by the categories
We need to compile all scores to the row for a singular employee
I had found this Removing Duplicates Article that seemed to at least process the information and compare it in a manner that I think this can be done, but am failing to make it work due to being inexperienced.
Did not know what Transpose was till someone commented :)
Here is the solution in another article for how to pull it off with Google Apps Script and with using the spreadsheet option.
How to split and transpose results over 2 columns
Here is the actual code I used to make it work(it is a little horrible but I tried) suggestions on how to improve this?:
function createScoreSheet() {
// Get Source spreadsheet
var source = SpreadsheetApp.getActive();
var sourceSheet = source.getActiveSheet();
var SourceActivate = sourceSheet.activate();
// Set Sheet Name
var sheetName = sourceSheet.getSheetName();
// Set Values to transpose and combine
var sourceEditor = sourceSheet.getRange("C1:C51");
var sourceWeb = sourceSheet.getRange("D1:D51");
var editorScores = sourceSheet.getRange("L1:L51");
var webScores = sourceSheet.getRange("K1:K51");
// Used to create a new spreadsheet
var sheetNameNew = sheetName + " Scores";
var createSheet = SpreadsheetApp.getActive().insertSheet(sheetNameNew,0);
var targetSheet = source.getSheetByName(sheetNameNew);
var totalScore = 1;
// s is the the counter we use to stick values into the rows
var s = 3;
// n is the the counter we use to stick values into the columns
var n = 1;
// loops through twice, once for the editor values, once for the webmaster
for (var j = 1; j<3; j++) {
if (j == 1) {
// grab values for the editors and copy to new sheet
sourceEditor.copyTo(targetSheet.getRange("A1"));
editorScores.copyTo(targetSheet.getRange("B1"));
// delete the header row then sort the column ASC by default
targetSheet.deleteRow(n);
targetSheet.sort(1);
// Find the last value to see how many scores we have
var lastRow = targetSheet.getLastRow();
}
if (j == 2) {
// grab values for the webmasters and copy to new sheet
sourceWeb.copyTo(targetSheet.getRange(n,1));
webScores.copyTo(targetSheet.getRange(n,2));
// delete the header row then sort the column ASC by default
targetSheet.deleteRow(n);
lastRow = targetSheet.getLastRow();
targetSheet.getRange(n,1,lastRow,2).sort(1);
lastRow = targetSheet.getLastRow();
}
// this loop will check to see if the value of the cell is equal to the next on the list and move the score
for (var i = 1; i<lastRow+1; i++) {
// Grab the name of the current row and the next
var firstName = targetSheet.getRange(n,1).getValue();
var nextName = targetSheet.getRange(n+1,1).getValue();
// Grab the scores
var oldScore = targetSheet.getRange(n+1,2);
var newScore = targetSheet.getRange(n,s);
// Loop to check to see if the firstname is blank and break to find the next value
if (firstName === "") {
break;
}
// checks to see if name is equal to the next then shifts then copies the score and adjust the horizontal position
if (firstName == nextName) {
totalScore = oldScore + newScore;
oldScore.copyTo(newScore);
s = s+1;
targetSheet.deleteRow(n+1);
}
// resets horizontal position for the score and increases the row
else {
s=3;
n=n+1;
}
}
// kills remaining rows
targetSheet.deleteRows(n,37);
}
}
I would do it like this:
If you want to generate the names automatically as well, then write this to the output sheet A1:
=unique('Example Data'!B2:B) - This function simply generate the editor names to the A2-A5 cells.
Now write this to the B2 cell:
=transpose(filter('Example Data'!E:E,'Example Data'!B:B=A2)) - This function filters the editor points according to the given name in the beginning of the row (in this case its A2). Then transposes the result in a horizontal form. To get the result for the other rows, simply populate this formula down.
I think you can find out the rest. :)
Hope it helps.