How do I compare Times in Google Apps Script - google-apps-script

I am trying to create a filter on a list based on a given time interval (2 cells in my spreadsheet which will input a timestamp C4 and C5). I have scoured the internet for a while and found out that the Javascript code used in Google Apps Script is different from the usual Javascript, so I haven't found a usable code snippet for my case.
The code is something like this:
var beginningTimeValue = new Date('2020-01-01 ' + ss.getRange("C4").getValue());
var endingTimeValue = new Date('2020-01-01 ' + ss.getRange("C5").getValue());
if(!beginningTimeValue == ""){
Logger.log(beginningDateValue);
unique = unique.filter(function(row)
{
const bTime = Date.parse(row[4]);
Logger.log(bTime);
Logger.log(beginningTimeValue);
return bTime.getTime() >= beginningTimeValue.getTime();
}
);
}
The value in row[4] is of DateTime value ("12/01/2021 00:03:35" for example). How do I filter this row out if I want the time to be between 08:00:00 and 13:00:00?

Three points:
To filter by two conditions instead of one, simply combine the two conditions with an && operator.
So:
return bTime.getTime() >= beginningTimeValue.getTime(); && bTime.getTime() <= endingTimeValue.getTime()
instead of
return bTime.getTime() >= beginningTimeValue.getTime();
Do not use both Date.parse() and getTime() simultaneously, since they both do the same.
const bTime = Date.parse(row[4]);
already returns you a timestamp in ms, if you try to apply
bTime.getTime()
on it - this will result in an error.
Be careful with
Logger.log(beginningDateValue);
given that your variable is called beginningTimeValue and not beginningDateValue.
Sample
Provided that beginningTimeValue, endingTimeValue and unique look like the harcoded values below, the following code snippet will work correctly for you:
function myFunction() {
var beginningTimeValue = new Date('2020-01-01 08:00:00');
var endingTimeValue = new Date('2020-01-01 13:00:00');
var unique = [["value","value","value","value","12/01/2021 00:03:35","value"],["value","value","value","value","01/01/2020 00:03:35","value"], ["value","value","value","value","01/01/2020 08:03:35","value"], ["value","value","value","value","01/01/2020 13:03:35","value"]]
if(!beginningTimeValue == ""){
Logger.log(beginningTimeValue);
unique = unique.filter(function(row)
{
const bTime = Date.parse(row[4]);
Logger.log(bTime);
Logger.log(beginningTimeValue);
return bTime >= beginningTimeValue.getTime() && bTime <= endingTimeValue.getTime();
}
);
console.log("unique: " + unique)
}
}
UPDATE
If you want to compare the times only (not the dates), you need to hardcode the date of row[4] to the same value like in beginningTimeValue and endingTimeValue. For this you can use the methods setDate(), setMonth and setYear.
Also, if your code should only work base don either vlaues are provided by a user in the cells C4 and C5 - you should adapt your code accordingly.
Be careful with your conditional statements: Even if ss.getRange("C4").getValue() is an empty string or an invalid input - beginningTimeValue will still not be an empty string, but rather the beginning of Unix time.
Sample:
function myFunction() {
var beginningTimeValue = new Date('2020-01-01 ' + ss.getRange("C4").getValue());
console.log("beginningTimeValue: " + beginningTimeValue)
var endingTimeValue = new Date('2020-01-01 ' + ss.getRange("C5").getValue());
console.log("endingTimeValue: " + endingTimeValue)
var unique = [["value","value","value","value","12/01/2021 00:03:35","value"],["value","value","value","value","01/01/2020 00:03:35","value"], ["value","value","value","value","01/01/2020 08:03:35","value"], ["value","value","value","value","01/01/2020 13:03:35","value"]]
if(!ss.getRange("C4").getValue() == ""){
Logger.log("beginningTimeValue: " + beginningTimeValue);
unique = unique.filter(function(row)
{
const bTime = new Date(row[4]);
bTime.setYear(2020);
// Be careful - months start in Javascript with 0!
bTime.setMonth(0);
bTime.setDate(1);
Logger.log(bTime);
if(ss.getRange("C5").getValue() != ""){
return bTime >= beginningTimeValue.getTime() && bTime <= endingTimeValue.getTime();
}
else{
return bTime >= beginningTimeValue.getTime();
}
}
);
console.log("unique: " + unique)
}
}
Keep im mind that Stackoverflow is not there to provide you a complete solution, but rather to help you troubleshooting and provide you references and samples that will guide you in the right direction. This will allow to incorporate the necessary modification into your code. You still need to have some basic understanding about coding to apply the sample to your script correctly.

Related

IF function in Google Sheets (scripts) with dates (today)

what could be wrong in these code?
...
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Daty'), true);
var today = spreadsheet.getRange('B1').getValue()
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('ZNC'), true);
var changeday = spreadsheet.getRange('E2').getValue()
var nrrej = spreadsheet.getRange('A2')
if(nrrej!=="" && changeday<today)
{Browser.msgBox("Info");
}else{
...
Script does not take these conditions into consideration together. Separately it is OK.
Answer
You are comparing a range with a string.
How to fix it
The variables today and changeday are values. They are defined as getRange('X').getValue(). However, nrrej is a range and you are comparing it with a string (""). The best approach to check if a cell is blank is using the function isBlank. It returns true if the range is totally blank.
Final code
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Daty'), true);
var today = spreadsheet.getRange('B1').getValue()
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('ZNC'), true);
var changeday = spreadsheet.getRange('E2').getValue()
var nrrej = spreadsheet.getRange('A2').isBlank()
if (nrrej == false && changeday < today) {
Browser.msgBox("Info");
} else { }
Some advice
Use the debugger to check the status of the variables before getting an error
Use automatic indentation with ctrl + shft + I
References
Range
Range: isBlank
Range: getValue
Use the debugger and breakpoints

How to find if the tasks for a particular date in a tasklist using google app-script

I am using an installed edit trigger of Google Sheets to create Google Tasks. However, when a row containing a task that has already been created as a Task is edited, a duplicate Task is created for the same day.
I'd like to find all the Tasks in a given list with a particular due date. Then I will be able to check their titles, to compare with the title of the task that would be created, so the script may decide if it should create a new task or update the existing one.
Here's my current triggered code:
function addTask(event){
if (spreadsheet.getActiveSheet().getName() === "Task List") {
var RowNum = event.range.getRow();
var taskproperties = spreadsheet.getActiveSheet().getRange(RowNum, 1, 1, 5).getValues();
var Title = taskproperties[0][1];
var Frequency = taskproperties[0][2];
var StartDate = taskproperties[0][3];
var Recurrence = taskproperties[0][4];
if (Title.trim().length !== 0 && Frequency.trim().length !== 0 &&
StartDate.toString().trim().length !== 0 && Recurrence.toString().trim().length !== 0)
{
//Code to Create a new task
//Code Get the task date
//Some codes to set Date parameters for use in script functions
//Some codes to set Date parameters for use in sheet functions
//Set the task parameters
//add task to list
//--------------------------------------------------------------
//Assign a cell in the spreadsheet for calculation of new dates for recurring task
var tempdatecell= spreadsheet.getSheetByName("Task List").getRange("F1")
//Insert new tasks based on the number of recurrence
for (i = 1; i < Recurrence; i++) {
//Insert a formula in a cell the spreadsheet to calculate the new task date
tempdatecell.setFormula('=WORKDAY.INTL("' + shTaskStartDate + '",' + i + '*VLOOKUP("' + Frequency + '",tasktype,2,false),"1000011")')
//Get task date from the cell
TaskDate = tempdatecell.getValue()
//Date parameters for use in script functions
var TaskDate = new Date(TaskDate);
var taskmonth = Number(TaskDate.getMonth())
var taskDay = TaskDate.getDate() + 1
var taskyear = TaskDate.getYear()
//Create a new task
var task = Tasks.newTask();
//Set the task parameters
task.title = Title;
task.due = new Date(taskyear, taskmonth, taskDay).toISOString()
//add task to list
task = Tasks.Tasks.insert(task, tasklistID);
}
tempdatecell.clearContent()
}
}
}
You might consider having your script write to another cell (probably in another column) that indicates the status of the task, such as added or updated and then write in a conditional statement that checks that cell to determine what to do with it. This is a really vague answer, but as Tanaike stated in their comment "provide your current script" or a generic version of it and we can be of greater help.
I have managed to find a work around which involves filtering the entire tasklist. It seems to work find with the few task that I have now. I am not sure how it will perform with a large volume of tasks. Any further contribution welcomes.
The code that I am using in the work around is as follows and replaces the line below //Create a new task in my original code:-
//Check if the task exist for the task date
var listoftasks = Tasks.Tasks.list(tasklistID)
var filtermonth="0" + shTaskStartMonth
var filterdate=scTaskStartYear + "-" + filtermonth.substr(filtermonth.length-2) + "-" + shTaskStartDay + "T00:00:00.000Z"
var filteredtask=listoftasks["items"].filter(function(item){
return item["due"]== filterdate && item["title"]===Title
})
if(filteredtask.length==0){
//Create a new task
var task = Tasks.newTask()
//Set the task parameters
task.title = Title;
task.due=new Date(scTaskStartYear,scTaskStartMonth,scTaskStartDay).toISOString()
//add task to list
task = Tasks.Tasks.insert(task, tasklistID)
}
else{
//Get the existing task
task = Tasks.Tasks.get(tasklistID, filteredtask[0].id)
task.setStatus("completed")
}
NB:- The setStatus does not work as expected but I will post a separate question for that.

Send email if cells' value is below certain value

I am absolutely new to this and pulling my hair out trying to make a script for work.
I need to check employee's certifications on a daily basis and have them re-certified if expired.
Here is a "testing" spreadsheet with random data: https://docs.google.com/spreadsheets/d/1vJ8ms5ZLqmnv4N1upNHD4SRfgIgIbEAAndvUNy-s9S4/edit?usp=sharing
It lists personnel working for my department along with their badge numbers and number of days their certifications are valid for. The original sheet takes the days value from another spreadsheet, but it shouldn't affect this (I think?).
What I'm trying to achieve is write a script that checks all numbers in C3:G24.
If any cell in this range has a value lower than 15, it should pull their badge number and name from the same row, along with the "days" their certificates are valid for and send an email containing all this data.
For example
Subject: Certifications about to expire
E-mail content: Your employee's (Name from col B) certification with Badge# (# from Col A) will expire in X days (where X = the number from range C3:G24).
So far my best attempt was to at least make it send ANY e-mail on edit, but failing miserably trying to adapt any script found online.
Here is what worked to at least send an e-mail but then I did something to break it:
function checkValue()
{
var ss = SpreadsheetApp.getActive();//not sure if needed, the spreadsheet eventually doesn't have to be open/active
var sheet = ss.getSheetByName("Certifications");
//not sure if this is ok
var valueToCheck = sheet.getRange("C3:G24").getValue();
//Here I'd like to change the "days" based on needs
if(valueToCheck < 15)
{
MailApp.sendEmail("email#company.com","Certifications","Your employee certification will expire soon" + valueToCheck+ ".");
}
}
Can someone please help guide me in the right direction?
here is what I would do:
function checkValue()
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Certifications");
var valueToCheck = sheet.getDataRange().getValues();
var resultValues = [];
valueToCheck = valueToCheck.filter(function(element){
var val = 0
if (parseInt(element[2]) < 15)
{
resultValues.push({col: "Cert1", value: element[2]})
return (true);
}
else if (parseInt(element[3]) < 15)
{
resultValues.push({col: "Cert2", value: element[3]})
return (true);
}
else if (parseInt(element[4]) < 15)
{
resultValues.push({col: "Cert3", value: element[4]})
return (true);
}
else if (parseInt(element[5]) < 15)
{
resultValues.push({col: "Cert4", value: element[5]})
return (true);
}
else if (parseInt(element[6]) < 15)
{
resultValues.push({col: "Cert5", value: element[6]})
return (true);
}
})
for(var i = 0; i < valueToCheck.length; i++)
{
MailApp.sendEmail("mail#company.com","Certifications","your employee's " + valueToCheck[i][1] + "with badge " + valueToCheck[i][0] + " certification will expire in " + resultValues[i].value + " days (column " + resultValues[i].col + ").");
}
}
use the getValues() function to retrieve datas.
then filter the values based on condtion of value being less than 15
at the same time grab the column name and the less than 15 data.
parse through both arrays to send datas to your mail

Detect formula errors in Google Sheets using Script

My ultimate goal is here, but because I've gotten no replies, I'm starting to learn things from scratch (probably for the best anyway). Basically, I want a script that will identify errors and fix them
Well, the first part of that is being able to ID the errors. Is there a way using Google Script to identify if a cell has an error in it, and return a particular message as a result? Or do I just have to do an if/else that says "if the cell value is '#N/A', do this", plus "if the cell value is '#ERROR', do this", continuing for various errors?. Basically I want ISERROR(), but in the script
Use a helper function to abstract away the nastiness:
function isError_(cell) {
// Cell is a value, e.g. came from `range.getValue()` or is an element of an array from `range.getValues()`
const errorValues = ["#N/A", "#REF", .... ];
for (var i = 0; i < errorValues.length; ++i)
if (cell == errorValues[i])
return true;
return false;
}
function foo() {
const vals = SpreadsheetApp.getActive().getSheets()[0].getDataRange().getValues();
for (var row = 0; row < vals.length; ++row) {
for (var col = 0; col < vals[0].length; ++col) {
if (isError_(vals[row][col])) {
Logger.log("Array element (" + row + ", " + col + ") is an error value.");
}
}
}
}
Using Array#indexOf in the helper function:
function isError_(cell) {
// Cell is a value, e.g. came from `range.getValue()` or is an element of an array from `range.getValues()`
// Note: indexOf uses strict equality when comparing cell to elements of errorValues, so make sure everything's a primitive...
const errorValues = ["#N/A", "#REF", .... ];
return (errorValues.indexOf(cell) !== -1);
}
If/when Google Apps Script is upgraded with Array#includes, that would be a better option than Array#indexOf:
function isError_(cell) {
// cell is a value, e.g. came from `range.getValue()` or is an element of an array from `range.getValues()`
const errorValues = ["#N/A", "#REF", .... ];
return errorValues.includes(cell);
}
Now that the v8 runtime is available, there are a number of other changes one could make to the above code snippets (arrow functions, etc) but note that changing things in this manner is not required.
Update: 25 March 2020
#tehhowch remarked "If/when Google Apps Script is upgraded with Array#includes, that would be a better option than Array#indexOf".
Array.includes does now run in Apps Script and, as anticipated provides a far more simple approach when compared to indexOf.
This example varies from the previous answers by using a specific range to show that looping through each cell is not required. In fact, this answer will apply to any range length.
The two key aspects of the answer are:
map: to create an array for each column
includes: used in an IF statement to test for a true or false value.
function foo() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sourcename = "source_sheet";
var source = ss.getSheetByName(sourcename);
var sourcerange = source.getRange("A2:E500");
var sourcevalues = sourcerange.getValues();
// define the error values
var errorValues = ["#NULL!", "#DIV/0!", "#VALUE!", "#REF!", "#NAME?", "#NUM!", "#N/A","#ERROR!"];
// loop though the columns
for (var c = 0;c<5;c++){
// create an array for the column
var columnoutput = sourcevalues.map(function(e){return e[c];});
// loop through errors
for (var errorNum=0; errorNum<errorValues.length;errorNum++){
// get the error
var errorvalue = errorValues[errorNum]
// Logger.log("DEBUG: column#:"+c+", error#:"+e+", error value = "+errorvalue+", does col include error = "+columnoutput.includes(errorvalue));
// if the error exists in this column then resposnse = true, if the error doesn't exist then response = false
if (columnoutput.includes(errorvalue) != true){
Logger.log("DEBUG: Column#:"+c+", error#:"+errorNum+"-"+errorvalue+" - No ERROR");
} else {
Logger.log("DEBUG: column#:"+c+", error#:"+errorNum+"-"+errorvalue+"- ERROR EXISTS");
}
}
}
return;
}
Shorter yet, use a nested forEach() on the [vals] array, then check to see if the cell value matches a value of the [errorValues] array with indexOf. This was faster than a for loop...
function foo() {
const errorValues = ["#NULL!", "#DIV/0!", "#VALUE!", "#REF!", "#NAME?", "#NUM!", "#N/A","#ERROR!"];
const vals = SpreadsheetApp.getActive().getSheets()[0].getDataRange().getValues();
vals.forEach((val,row) => { val.forEach((item, col) => {
(errorValues.indexOf(item) !== -1) ? Logger.log("Array element (" + row + ", " + col + ") is an error value.") : false ;
});
});
}
I had a similar question and resolved using getDisplayValue() instead of getValue()
Try something like:
function checkCells(inputRange) {
var inputRangeCells = SpreadsheetApp.getActiveSheet().getRange(inputRange);
var cellValue;
for(var i=0; i < inputRangeCells.length; i++) {
cellValue = inputRangeCells[i].getDisplayValue();
if (cellValue=error1.....) { ... }
}
}
Display value should give you what's displayed to the user rather than #ERROR!

Identifying Form destination (Spreadsheet AND SHEET)

I'm working on a script that interacts with Google Form' response sheet.
FormApp.getActiveForm().getDestinationId()
give me the spreadsheet id, but I don't find a way to get the sheet itself. User can change its name and position, so I need to get its id, like in
Sheet.getSheetId()
I also have to determine the number of columns the responses uses. It's not equal to the number of questions in the form. I can count the number of items in the form:
Form.getItems().length
and then search for gridItems, add the number of rows in each and add them minus one:
+ gridItem.getRows().length - 1
Finally, I think there's no way to relate each question with each column in the sheet, but by comparing somehow columns names with items title.
Thank you
#tehhowch came very close to the correct answer, but there is a problem with the code: there is no guarantee that form.getPublishedUrl() and sheet.getFormUrl() will return exactly the same string. In my case, form.getPublishedUrl() returned a URL formed as https://docs.google.com/forms/d/e/{id}/viewform and sheet.getFormUrl() returned https://docs.google.com/forms/d/{id}/viewform. Since the form id is part of the URL, a more robust implementation would be:
function get_form_destination_sheet(form) {
const form_id = form.getId();
const destination_id = form.getDestinationId();
if (destination_id) {
const spreadsheet = SpreadsheetApp.openById(destination_id);
const matches = spreadsheet.getSheets().filter(function (sheet) {
const url = sheet.getFormUrl();
return url && url.indexOf(form_id) > -1;
});
return matches.length > 0 ? matches[0] : null;
}
return null;
}
There is now a way to verify which sheet in a Google Sheets file with multiple linked forms corresponds to the current Form - through the use of Sheet#getFormUrl(), which was added to the Sheet class in 2017.
function getFormResponseSheet_(wkbkId, formUrl) {
const matches = SpreadsheetApp.openById(wkbkId).getSheets().filter(
function (sheet) {
return sheet.getFormUrl() === formUrl;
});
return matches[0]; // a `Sheet` or `undefined`
}
function foo() {
const form = FormApp.getActiveForm();
const destSheet = getFormResponseSheet_(form.getDestinationId(), form.getPublishedUrl());
if (!destSheet)
throw new Error("No sheets in destination with form url '" + form.getPublishedUrl() + "'");
// do stuff with the linked response destination sheet.
}
If you have unlinked the Form and the destination spreadsheet, then obviously you won't be able to use getDestinationId or getFormUrl.
I needed this also, and remarkably there is still no apps script method that facilitates it. In the end I set about finding a reliable way to determine the sheet id, and this is what I ended up with by way of programmatic workaround:
Add a temporary form item with a title that's a random string (or something similarly suitable)
Wait for the new corresponding column to be added to the destination sheet (typically takes a few seconds)
Look though each sheet in the destination until you find this new form item title string in a header row
Delete the temporary form item that was added
Wait for the corresponding column in the sheet to unlink from the form and become deletable (typically takes a few seconds)
Delete the column corresponding to the temporary form item
Return the sheet ID
I'm sure some won't like this approach because it modifies the form and spreadsheet, but it does work well.
With the necessary wait times included it takes about 12 seconds to perform all the look up / clean up operations.
Here's my code for this method in case anyone else might like to use it.
// Takes Apps Script 'Form' object as single paramater
// The second parameter 'obj', is for recursion (do not pass a second parameter)
// Return value is either:
// - null (if the form is not linked to any spreadsheet)
// - sheetId [int]
// An error is thrown if the operations are taking too long
function getFormDestinationSheetId(form, obj) {
var obj = obj || {}; // Initialise object to be passed between recursions of this function
obj.attempts = (obj.attempts || 1);
Logger.log('Attempt #' + obj.attempts);
if (obj.attempts > 14) {
throw 'Unable to determine destination sheet id, too many failed attempts, taking too long. Sorry!';
}
obj.spreadsheetId = obj.spreadsheetId || form.getDestinationId();
if (!obj.spreadsheetId) {
return null; // This means there actually is no spreadsheet destination set at all.
} else {
var tempFormItemTitle = '### IF YOU SEE THIS, PLEASE IGNORE! ###';
if (!obj.tempFormItemId && !obj.sheetId) { // If the sheet id exists from a previous recusion, we're just in a clean up phase
// Check that temp item does not already exist in form
form.getItems(FormApp.ItemType.TEXT).map(function(textItem) {
var textItemTitle = textItem.getTitle();
Logger.log('Checking against form text item: ' + textItemTitle);
if (textItemTitle === tempFormItemTitle) {
obj.tempFormItemId = textItem.getId();
Logger.log('Found matching form text item reusing item id: ' + obj.tempFormItemId);
}
return 0;
}); // Note: Just using map as handy iterator, don't need to assign the output to anything
if (!obj.tempFormItemId) {
Logger.log('Adding temporary item to form');
obj.tempFormItemId = form.addTextItem().setTitle(tempFormItemTitle).getId();
}
}
obj.spreadsheet = obj.spreadsheet || SpreadsheetApp.openById(obj.spreadsheetId);
obj.sheets = obj.sheets || obj.spreadsheet.getSheets();
obj.sheetId = obj.sheetId || null;
var sheetHeaderRow = null;
for (var i = 0, x = obj.sheets.length; i < x; i++) {
sheetHeaderRow = obj.sheets[i].getSheetValues(1, 1, 1, -1)[0];
for (var j = 0, y = sheetHeaderRow.length; j < y; j++) {
if (sheetHeaderRow[j] === tempFormItemTitle) {
obj.sheetId = obj.sheets[i].getSheetId();
Logger.log('Temporary item title found in header row of sheet id: ' + obj.sheetId);
break;
}
}
if (obj.sheetId) break;
}
// Time to start cleaning things up a bit!
if (obj.sheetId) {
if (obj.tempFormItemId) {
try {
form.deleteItem(form.getItemById(obj.tempFormItemId));
obj.tempFormItemId = null;
Logger.log('Successfully deleted temporary form item');
} catch (e) {
Logger.log('Tried to delete temporary form item, but it seems it was already deleted');
}
}
if (obj.sheetId && !obj.tempFormItemId && !obj.tempColumnDeleted) {
try {
obj.sheets[i].deleteColumn(j + 1);
obj.tempColumnDeleted = true;
Logger.log('Successfully deleted temporary column');
} catch (e) {
Logger.log('Could not delete temporary column as it was still attached to the form');
}
}
if (!obj.tempFormItemId && obj.tempColumnDeleted) {
Logger.log('Completed!');
return obj.sheetId;
}
}
SpreadsheetApp.flush(); // Just in case this helps!
// Normally this process takes three passes, and a delay of 4.5 secs seems to make it work in only 3 passes most of the time
// Perhaps if many people are submitting forms/editing the spreadsheet, this delay would not be long enough, I don't know.
obj.delay = ((obj.delay || 4500));
// If this point is reached then we're not quite finished, so try again after a little delay
Logger.log('Delay before trying again: ' + obj.delay / 1000 + ' secs');
Utilities.sleep(obj.delay);
obj.attempts++;
return getFormDestinationSheetId(form, obj);
}
}
To get the spreadsheet, once you have the DestinationID, use SpreadsheetApp.openById(). Once you have that, you can retrieve an array of sheets, and get the response sheet by index, regardless of its name.
var destId = FormApp.getActiveForm().getDestinationId();
var ss = SpreadsheetApp.openById(destId);
var respSheet = ss.getSheets()[0]; // Forms typically go into sheet 0.
...
From this point, you can manipulate the data in the spreadsheet using other Spreadsheet Service methods.
I also have to determine the number of columns the responses uses. It's not equal to the number of questions in the form. I can count the number of items in the form... (but that doesn't match the spreadsheet)
You're right - the number of current items does not equal the number of columns in the spreadsheet. The number of columns each response takes up in the destination sheet includes any questions that have been deleted from the form, and excludes items that are not questions. Also, the order of the columns in the spreadsheet is the order that questions were created in - as you re-arrange your form or insert new questions, the spreadsheet column order does not reflect the new order.
Assuming that the only columns in the spreadsheet are from forms, here's how you could make use of them:
...
var data = respSheet.getDataRange().getValues(); // 2d array of form responses
var headers = data[0]; // timestamp and all questions
var numColumns = headers.length; // count headers
var numResponses = data.length - 1; // count responses
And your last point is correct, you need to correlate names.
Finally, I think there's no way to relate each question with each column in the sheet, but by comparing somehow columns names with items title.