Very strange Google Apps Script behavior - google-apps-script

I am seeing some very strange behavior with Google Apps Script. All I am trying to do is parse some rows in my sheet and if for any row, if the date is > 1/1/1999 then add a dropdown which pre populates some Error Value. The strange behavior here is that this always populates the dropdown one row above the row which satisfies the date check condition. So for example if row #5 satisfies the condition, the dropdown is populated in row #4. Not sure why this is happening. I also tried to do this
var range = sheet.getRange(i+1, 14);
but that doesn't seem to show anything on the screen. I am unable to understand this strange behavior. Any help will be appreciated.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[1];
var dataRange = sheet.getDataRange();
var values = dataRange.getDisplayValues();
for (var i = rowNum; i < values.length; i++) {
if (new Date(values[i][9]).getTime() > new Date('1/1/1999').getTime()){
SpreadsheetApp.getUi().alert(values[i][2]);
var range = sheet.getRange(i, 14);
var rule = SpreadsheetApp.newDataValidation().requireValueInList(['Error Value'], true).build();
range.setDataValidation(rule);
}

Use
var range = sheet.getRange(Number(i)+1, 14);
When you are using i+1 and when i=20, i+1 will become 201 and you will not seeing any visible difference on the screen. row 201 will be far down your screen.
In JavaScript, numbers and strings will occasionally behave in ways you might not expect. It is always good to take precaution before doing operation on variables.
Also read https://autotelicum.github.io/Smooth-CoffeeScript/literate/js-intro.html#operations-on-numbers-strings

Your data validation is set to null. It should be Range.setDataValidation(rule)
that is why you dont see a dropdown box.
This function
Range.getDisplayValues()
Returns an array and index of an array starts at 0. However, in a spreadsheet the row index starts at 1. So you need to add 1 to convert array index to a row index. Use the following to get the correct range
Sheet.getRange(i+1,14)

Related

Exception: The starting row of the range is too small - Logs Numbers Why?

I have a basic script to take numbers from a sheet and use them to create a range, as well as using the last column function. I have had the error range is too small for the posting range.
When I log the output for both the column and row numbers these come out as expected!
I thought initially, it was because one was a last column pull and the other was pulling an integer from the cell in the sheets, as they were coming with decimal places, so I have overcome this with the conversion to number and then removing the decimals with the .tofixed() but this does not work either. Any ideas?
function weeklyData() {
var sourcess = SpreadsheetApp.openById('1B93Oq2s9Nou5hVgOb3y3t15t9xnqRMBnrYkAed-oxrE'); // key of source spreadsheet
var sourceSheet = sourcess.getSheetByName('Measures & Answers'); // source sheet name - change to your actual sheet name
var lr = Number(sourceSheet.getRange(2,3).getDataRegion(SpreadsheetApp.Dimension.ROWS).getLastRow()).toFixed(0);
for(var i=0;i<lr+1;i++){
var dataValue = sourceSheet.getRange(i+2,3).getValue(); //This weeks numbers to update into table
var rowdataRange = sourceSheet.getRange(i+2,4).getValue(); //The row that the data needs to be pasted
var rowformat = Number(rowdataRange);
var row = rowformat.toFixed(0);
var pasteSheet = sourcess.getSheetByName('WHERE DATA ENDS UP'); // Data is to be pasted - change to your actual sheet name
var pasteColumn = pasteSheet.getRange(12,12).getDataRegion(SpreadsheetApp.Dimension.COLUMNS).getLastColumn()+1;
var column = pasteColumn.toFixed(0); // Column that is free for this weeks data
var pasteRange = pasteSheet.getRange(row,column,1,1);
Logger.log(pasteRange);
// pasteRange.setValue(dataValue);
}};
Your script works fine for me. I suspect this is an example script you've adapted from somewhere and trying to apply it to your data structure?
The reason you are getting the error is probably because the data in column 4 of your source sheet is not of number format? Either change your data or change the following line to the column containing numeric values.
var rowdataRange = sourceSheet.getRange(i+2,4).getValue();
This script is poorly written for this particular use.
You might want to check your Spreadsheet since it has lots of random values at random ranges.
When you use the following command Logger.log(pasteSheet.getLastColumn()); the number returned is 3753: which means that that is the next available column at which your data will be pasted.
The error message you were getting is due to the fact that the range was incorrect and you were passing wrong values in order to access it, which was most likely because of the values mentioned above.
Moreover, after cleaning all the unnecessary data, you can make use of the below script.
Snippet
function weeklyData() {
var spreadsheet = SpreadsheetApp.openById("ID_OF_THE_SPREADSHEET");
var sourceSheet = spreadsheet.getSheetByName("Measures & Answers");
var pasteSheet = spreadsheet.getSheetByName("WHERE DATA ENDS UP");
var valsToCopy = sourceSheet.getRange(2, 3, sourceSheet.getLastRow(), 1).getValues();
var rowsAt = sourceSheet.getRange(2, 4, sourceSheet.getLastRow(), 1).getValues();
var column = pasteSheet.getRange(12,12).getDataRegion(SpreadsheetApp.Dimension.COLUMNS).getLastColumn()+1;
for (var i = 0; i < valsToCopy.length; i++)
if (rowsAt[i][0] != "")
pasteSheet.getRange(parseInt(rowsAt[i][0]), parseInt(column)).setValue(valsToCopy[i][0].toString());
}
Explanation
The above script gathers all the data that needs to be copied as well as the rows associated with it. In order to make sure you don't end up using inappropriate values for the ranges, an if condition has been placed to make sure the value is not empty.
Reference
Sheet Class Apps Script - getLastColumn();
Sheet Class Apps Script - getRange(row, column, numRows, numColumns);

How can a custom sorting script be prevented from eating header row?

I modified the example code provided here to come up with the script below. While it does the job, I have been unable to find a way to prevent it from deleting the header (first) row of the sheet.
A somewhat tangential aside: the following two lines of code at the end of the cited script were removed to sort the sheet without deleting its contents and replacing them with an unformatted array:
sheet.clear();
sheet.getRange(1,1,rows,cols).setValues(vf);
I tried skipping it in all the ranges but either get errors about mismatches between ranges and data or other problems.
function status_sort(){
var ss=SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('current');
var rows = sheet.getLastRow();
var cols = sheet.getLastColumn();
var columnToSortBy = 4;
var rg = sheet.getRange(2, columnToSortBy, sheet.getLastRow(), 1);
var v1 = rg.getValues();
var sortObj = {'black_box':1, 'purple_box':2, 'green_box':3, 'yellow_box':4, 'red_box':5,'blue_box':6};
var col = [];
for(var i = 0; i < v1.length; i++){
col.push([sortObj[v1[i]]]);
}
sheet.getRange(2, cols + 1, rows, 1).setValues(col);
sheet.getDataRange().sort([{column:sheet.getLastColumn(),ascending:true}]);
var vf = sheet.getDataRange().getValues();
for(var i = 0; i < vf.length; i++){
vf[i].splice(vf[i].length,1);
}
}
It looks like v1 contains your data (no headers) and you put keys into col which gets appended as an additional column for sorting purposes only, then you sort the whole thing by the keys. If I'm reading it right your headers ought to be somewhere in the sorted data, since I don't think getDataRange is smart enough to ignore your headers.
I wonder what would happen if you replaced:
sheet.getDataRange().sort([{column:sheet.getLastColumn(),ascending:true}]);
with:
sortThis = sheet.getRange(2, 1, rows - 1, cols + 1)
sortThis.sort([{column:sheet.getLastColumn(),ascending:true}]);
// getRange parameters:
// 2 : skip a row for the headers
// 1 : start with the leftmost column
// rows - 1 : get one fewer than the original row count (to exclude the headers)
// cols : get one more than the original column count (to include the extra)
As an aside, I think that the loop at the end grabs everything in the sheet and then removes the last column (which was only injected for sorting purposes), and the code that you removed replaces the sheet with the result. If you're not going to call setValues(vf) then you may as well omit any other code that reference vf too.
Based on https://stackoverflow.com/a/42224466/1054322 I wonder if you might want to use getDisplayValues() instead. I didn't see a setDisplayValues() in the API reference, so I'm going to guess that your formatting is getting lost on read, and not write, and further that setValues() will not also clobber your formatting. Not sure if this will interfere with the sorting though, since the displayValues might wrap the data and therefore sort differently.

Google app script: find first empty cell in a row

Im learning Google app script while building a dashboard. I'm collecting data from several sheets. My goal is to see by how many rows each sheet grows every week. This gives me insight in how my business is doing.
I can get the length of all the sheets I want to check, however I cant find any code which helps me to find the first empty cell in a specific row. I want to place the length of each sheet there (in my dashboard datacollection sheet) to create a graphs later on.
What I have is:
var range = ss.getRange(2, 1, 1, 1000);
var waarden = range.getValues();
Logger.log(waarden);
var counter = 0
for (var j = 0; j < ss.getLastColumn(); j++) {
Logger.log(waarden[0][j]);
if (waarden[0][j] == ""){
break
} else {
counter++;
}
Logger.log(counter);
}
This works but I can't image this being the best solution (or quickest solution). Any tips in case my length goes beyond 1000 without me noticing it (although it would take a couple of years to do so in this case ;) )?! Why does getLastColumn() behave so much different than getLastRow()?
Thanks for helping me learn :)
*** edited I figured out I have to use if (waarden[0][j] === ""){ with three = otherwise if my sheet in the row that I use as a check has a length of 0 than this is also counted as empty with two =operators.
Try indexOf()
function firstEmptyCell () {
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
var range = ss.getRange(2, 1, 1, ss.getMaxColumns());
var waarden = range.getValues();
// Get the index of the first empty cell from the waarden array of values
var empty_cell = waarden[0].indexOf("");
Logger.log("The index of the first empty cell is: %s", empty_cell);
}
This will give you the column position of the empty cell starting from a 0 index. So if the returned index is 4, the column is "E".
edit: As for the getLastColumn() question; you could use getMaxColumns() instead. Updated code to get all columns in the sheet.

Autofill google forms based on user input

Alright stack friends,
I'm working on my first projects using google scripts and it's been pretty fun so far. My project is to create a form for data entry that can either accept an ID number and fill in the rest of the fields, or let the user fill out the entire form. Basically my method to fill in the other fields is just to have a lookup table on the second sheet. When the user submits a form, the script runs, looks for the ID of the last row, scans the reference table for the ID, and then fills in the details.
I think the problem I'm having is the assumption that the data from the form is already in the sheet when the script runs. The problem I noticed is that the script sometimes fails to fill in the gaps. I tried creating form submissions in a loop with the same ID and they function somewhat erratically but it seems like the last sumbission always works which would make sense if the script executions are not matching up with the form submissions. Here's the script for reference:
function fillGaps() {
// First take in the appropriate spreadsheet objects and get the sheets from it
var ss = SpreadsheetApp.openById(id);
var sheet = ss.getSheets()[0];
var refSheet = ss.getSheets()[1];
// Here's the last rows' index
var lastRow = sheet.getLastRow();
var lastRowRef = refSheet.getLastRow();
// now this is an array of values for the last row and the student ID entered
var response = sheet.getRange(lastRow, 1, 1, 7).getValues();
var enteredID = response[0][1];
// Next we're going to try to load up the lookup table and scan for the ID
var stuIDs = refSheet.getRange(2, 4, refSheet.getLastRow()).getValues();
var row = 0;
while(enteredID != stuIDs[row] && row <= lastRowRef){
row++;
}
// Okay at this point the row variable is actually -2 from what the sheet index
// is that I'm thinking of. This is because we didn't load the first row (names)
// and the way arrays are indexed starts with 0.
row++;
row++;
// now assuming that it found a match we'll fill in the values
if(row < refSheet.getLastRow()){
// Alright now we need to wrangle that row and format the data
var matchedRow = refSheet.getRange(row, 1, 1, 6).getValues();
// modify the response
var replacement = [response[0][0],enteredID, matchedRow[0][1],matchedRow[0][0],matchedRow[0][2],matchedRow[0][4],matchedRow[0][5]];
sheet.getRange(lastRow, 1, 1, 7).setValues([replacement]) ;
}
}
So I'm wondering:
Does this seem like the right diagnosis?
If so, what would be the best way to remedy? I thought of adding a little delay into the script as well as trying to capture the submissions timestamp (not sure how to do that)
Thank you much!
The following code gives a 2D array:
var stuIDs = refSheet.getRange(2, 4, refSheet.getLastRow()).getValues();
Also,refSheet.getLastRow gives the last row, lets say it is 10 in this case. The syntax for getRange is getRange(row, column, numRows) and the last argument is the number of rows, not the last column. So in the above code the selected range would be row 2 - 11 rather than 2- 10. Unless that is what you intended, modify the code like so:
var stuIDs = refSheet.getRange(2, 4, refSheet.getLastRow()-1).getValues();
To access the values in stuIDs you should use stuIDs[row][0] (2D array) to check for matching ID. Assuming your ID was to be matched was in column 1.
Secondly, in the loop you are using the following to check for the last index in array row <= lastRowRef which will cause it go out of range(because array starts at 0 and sheet row at 1) instead use this row < stuIDs.length
Finally, in case you don't find a match you will end up with the last row and your code will end you taking the last row as the matched index. This can be prevented by using a boolean variable to check for a match.
var foundId = false
var row = 0;
var i = 0;
for (i in stuIDs){
if(stuIDs[i][0] == enteredID)
foundID = true
break
}
}
row = i + 2
if (foundID){
var matchedRow = refSheet.getRange(row, 1, 1, 6).getValues();
// modify the response
var replacement = [response[0][0],enteredID, matchedRow[0][1],matchedRow[0][0],matchedRow[0][2],matchedRow[0][4],matchedRow[0][5]];
sheet.getRange(lastRow, 1, 1, 7).setValues([replacement]) ;
}
PS: You can also use event objects to get the values of response (eventObj.values). As mentioned here: https://developers.google.com/apps-script/guides/triggers/events
Hope that helps!

place objects in an array directly in different column as string in google apps script

i'm quite a noob when it comes to programming and google apps script. Hope someone can help me out since the headache is becoming unbearable ;)
The situation:
1 spreadsheet,
2 sheets: Sheet1 & Sheet2
in Sheet1, first column, I have my data which contains text as email plainBody.
in my code i use a regular expression to get the exact data from the e-mail that i want, this works fine.
The issue:
I'can't find a way to set the values of this result into column B of Sheet2.
I don't want to first place the results in columnA and then copy it to columnB.
Hope you guys can help me out :) I've googled my ass off but didn't find a solution..
Current code i'm using:
function emailExtract(){
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1'); //get active spreadsheet and sheetname
var sheet2 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet2');//get active spreadsheet and sheetname
var rangeA = sheet2.getRange("A1:A");//range to later on set the numberFormat, not important
var rangeB = sheet2.getRange("B1:B");//range to later on set the numberFormat, not important
sheet2.clear();
var ss = sheet.getDataRange();//define the range that includes data
var values = ss.getValues();// get the values (plainBody email), 153 rows of data (at the moment).
var regex = /\d+,\d*/; //Regular expression to extract the numbers i need.
for (var i = 0;i < values.length;i++){
var eurMoney = regex.exec(values[i]);//execute the regular expression on every row with data.
sheet2.appendRow([eurMoney.toString()]);//this places the values in columnA, I need these values to appear in columnB!
}
rangeA.setNumberFormat("€0.00");
rangeB.setNumberFormat("€0.00");
}
Instead of using:
sheet2.appendRow([eurMoney.toString()]);
use getLastRow on the data Range of sheet2 (sheet2.getDataRange). Then, using
sheet2.getRange(getLastRow+1, 2).setValue(eurMoney)
in place of the line you mentioned in your code. Hence, you for loop would look something like this;
for (var i=0; i<values.length; i++)
{
var range = sheet2.getDataRange();
var lRow = range.getLastRow();
sheet2.getRange(lRow+1, 2).setValue(eurMoney);
}
However, please note getValues outputs a 2 dimensional array. In your implementation, I don't believe it is being implemented correctly. So I would leave it to your better judgement to work on that.