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.
Related
I need to add Data Validation to a range of cells based on the State chosen in the previous cell.
I've run this code which works ok for a limited amount of data but is not working on the actual spreadsheet.
function onEdit(e) { // Runs automatically when the user edits the sheet
var value = e.value; // Get the new value entered into the edited cell
var col = e.range.getColumn(); // Get the column number of the edited cell
var row = e.range.getRow(); // Get the row number of the edited cell
if (col == 6 && row >= 10 && row <= 854) { // Make sure that the edited cell is part of the table
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Hoja 1');
var sheet2 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Localidades'); // Get the sheet that has the table with the list of cities
var dropdownData = sheet2.getRange("A1:X594").getValues(); // Get the table with the list of cities.
var listOfCountries = dropdownData[0]; // The top row is the list of countries
var countryColumn = listOfCountries.indexOf(value); // Find the column in which the country name appears
if (countryColumn != -1) { // If the new country name is in the list
var cityList = [];
for (var dataRow = 1; dataRow < dropdownData.length; dataRow++) {
cityList.push(dropdownData[dataRow][countryColumn]);
}
var cityCell = sheet.getRange(row, col + 1);
cityCell
.clearDataValidations() // Remove any existing data validation in the target cell
.clearContent(); // Clear the cell
var rule = SpreadsheetApp.newDataValidation().requireValueInList(cityList, true).build();
cityCell.setDataValidation(rule);
}
}
}
I've debugged and it gets the CityList all right so don't know what's the problem really. Any help??
There is a limited amount of allowed dropdown options
Through testing you can easily verify that it is 500 options.
So, if you hardcode "A1:X594" you are above the limit.
However, for most of the provinces in your data the amount of options is less than 594 and your array contains many empty values.
You can remove all duplicates inlcuding emoty values by filtering, e.g.
cityList = cityList.filter(function (value, index, self) {
return self.indexOf(value) === index;
})
As for the provinces where you have many entries (e.g. Buenos Aires), maybe you can try to subdivide it into smaller regions so you have less than 500 dropdown options per dropdown?
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 1 year ago.
Improve this question
I was presented with a problem where static fields were being modified next to dynamic fields in a Google Spreadsheet. The issue being sometimes the column with dynamic data would change and the static fields would no longer apply to them.
I can't think of a way to handle this except for a script, but my scripting knowledge is limited at best.
The idea is to have a sheet that uses =importrange() that would house the dynamic data. Then a second sheet would have the static data which would have rows added or removed based on whether they match the dynamic sheet.
For example:
The dynamic sheet would be called "Source" and in the first column we would have the names: Peter, Brian, Louis, Chris, Stewie
The static sheet would be called "Target" and in the first column we would have the names: Peter, Brian, Louis, Chris, Meg
The idea would be to remove the row that has "Meg" in the first column and then insert a row (or add to the bottom of the range) the value of "Stewie".
End goal is to programmatically make the static list match the dynamic list.
After piecing together how I thought the process should go I was able to throw this together. I have two functions that get called by a third one. The first function is used to grab a 2d array of whichever sheet I need. The assumption here is the arrays that I'll be comparing to each other will all be in the first column.
function grabSheetInformation(sheet) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetInfo = ss.getSheetByName(sheet);
var range = sheetInfo.getRange("A2:A").getValues();
var rangeMax = range.filter(String).length;
range = sheetInfo.getRange("A2:A" + (rangeMax + 1)).getValues();
return range;
}
The second function that I use just sorts the data after everything is said and done.
function sortTargetData() {
var sheetToSort = "Target"; // sheet name that is being sorted
var columnToSortBy = 1; // column A = 1, B = 2, etc.
var rangeToSort = "A2:R";
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetToSort);
var range = sheet.getRange(rangeToSort);
range.sort( { column : columnToSortBy, ascending: true } );
}
This is where it all comes together.
function mergeData() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sourceInfo = grabSheetInformation("Source");
var targetInfo = grabSheetInformation("Target");
var sourceMax = sourceInfo.length;
var targetMax = targetInfo.length;
SpreadsheetApp.setActiveSheet(ss.getSheetByName("Target"));
var sheet = ss.getActiveSheet();
var array = [];
var offset = 1;
for (var i = 0; i < targetMax; i++) {
array.push(targetInfo[i][0]);
}
for (i = 0; i < sourceMax; i++) {
if(array.indexOf(sourceInfo[i][0]) == -1) {
sheet.insertRowAfter(targetMax + offset);
sheet.getRange("A" + (targetMax + offset + 1)).setValue(sourceInfo[i][0]);
offset++;
}
}
// reset variables
sourceInfo = grabSheetInformation("Source");
targetInfo = grabSheetInformation("Target");
sourceMax = sourceInfo.length;
targetMax = targetInfo.length;
sheet = ss.getActiveSheet();
array = [];
for (i = 0; i < sourceMax; i++) {
array.push(sourceInfo[i][0]);
}
for (i = targetMax -1; i >= 0; i--) {
if(array.indexOf(targetInfo[i][0]) == -1) {
sheet.deleteRow(i+2);
}
}
sortTargetData();
}
As I posted in my initial question, the purpose of the script is to allow a user to have a dynamic list that they can add static values to.
The layout of the spreadsheet would have two tabs, "Source" and "Target". The "Source" tab would have the dynamic data in the first column with a header. For my use-case it would be pulled in using "=importrange()". The "Target" tab would have the first row contain headers and be updated via the script.
If there were values in the "Source" tab that weren't in the "Target" tab then rows would be added to the end of the list on the "Target" tab with the missing values added.
If there were values in the "Target" tab that weren't in the "Source" tab then the offending rows would be deleted.
My organization has multiple spreadsheets with employee names all over the place. The benefit of this would be to have a single list of names that can be updated and spreadsheets would automatically update with the most up-to-date list.
There is also the assumption that names won't be duplicated.
Spreadsheet that I created to test this out on:
Shared View Only Spreadsheet
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) {
...
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.');
}
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);