JavaScript: global array variable returned undefined - javascript

JavaScript newbie here. I've searched and searched for answers and can't seem to figure this out. The arrays that I'm passing to a function aren't being passed as references properly. I don't think this is an async issue like many posts allude to but I could be wrong.
I have global arrays that I'm passing to a function. Inside the function, the arrays return their proper values, but when I try to access them outside of the function, they are undefined.
For context, I'm passing 3 arrays that hold the dry-bulb temperature, wet-bulb temperature, and hour that the measurements were taken for later calculations. I've only included a few sample data points for brevity. Sample code below:
function run(){
var hour = [];
var db = [];
var wb = [];
var cities = ["AB Edmonton","MI Detroit"];
getData(hour, db, wb, cities);
//this shows undefined, although within getData it is accurate data
alert(hour[1]);
}
function getData(hour, db, wb, cities){
//i= drop-down selection index, set to zero for testing
i=0;
switch(cities[i]) {
case "AB Edmonton":
hour = [1,2,3];
db = [15,18,21];
wb = [10,13,20];
break;
//case "MI Detroit":....
}
//this shows accurate values in the alert window
alert(cities[i] + " at hour:" + hour[i] + " the temp is:" + db[i]);
return [hour, db, wb];
};

run assigns empty arrays to hour, db and wb. These are variables which are locally scoped to the run function.
It then calls getData and passes those arrays as arguments.
Inside getData new local variables (also named hour, db and wb) are declared and are assigned the three empty arrays that were passed when the function was called.
The function then ignores those values and overwrites them with new arrays (these ones have contents).
It then returns another new array which holds each of those arrays.
This brings us back to run. The return value of getData is ignored completely and the original arrays (which are still stored in the hour, db and wb variables that belong to run) are accessed (but they are still empty).
You can either:
Manipulate the existing arrays inside getData instead of overwriting them. (e.g. hour = [1,2,3] may become hour.push(1); hour.push(2); hour.push(3)).
Use the return value of getData (in which case you don't need to bother assigning values or passing the empty arrays in the first place). You could use an object instead of an array so you can have useful names instead of an order here too.
Such:
function run(){
var cities = ["AB Edmonton","MI Detroit"];
var data = getData(cities);
alert(data.hour[1]);
}
function getData(cities){
//i= drop-down selection index, set to zero for testing
var i=0; // Use locally scoped variables where possible
var hour, db, wb;
switch(cities[i]) {
case "AB Edmonton":
hour = [1,2,3];
db = [15,18,21];
wb = [10,13,20];
break;
//case "MI Detroit":....
//this shows accurate values in the alert window
alert(cities[i] + " at hour:" + hour[i] + " the temp is:" + db[i]);
return { hour: hour, db: db, wb: wb];
};

Well, those aren't global variables. The one hour variable is local to run() in which it is declared with var, the other is local to getData in which it is declared as a parameter.
In your getData function you are overwriting the local variable (which initially has the value that was passed in by run()) in the line
hour = [1,2,3];
and from thereon the two variables refer to different arrays.

function getData(hour, db, wb, cities){ }
hour, db, etc are references to the initial Arrays.
When you write hour = [1,2,3];, the hour local references does not longer point to your desired Array, but to a new Array which you have just constructed: [1,2,3]. To fix this issue simply push values to the parameters
hours.push(1,2,3); so you won't overwrite your references.
This is the same problem that occurs when you do:
a = {x : 1};
function setX(obj) {
obj = {x: 2};
}
function correctSetX(obj) {
obj.x = 2;
}
The setX function will do nothing, while the correctSetX will correclty a to {x : 2}.

Thank you all for your help! I've posted how I edited my code to get it to work based on the comments. A couple things:
-I've moved all variables to be local in the getData() function. At least one of the comments gave the impression that it is better practice to keep variables local (forgive me, I am not a CSE guy by training, but I appreciate the tips and patience on your behalf)
-I wasn't able to simply use the .push method because the amount of data caused an error. (there are at least 8760 measurements per year) I can't remember the exact error but it was related to stack limits
-At the suggestion of Quentin, I instead created a dataSet object that had array properties. This object is what is returned by the getData function. Thank you again, this was a much better way to handle this
Sample below (with limited data):
function run(){
//get data
var dataSet = getData();
//test the result on the 2 hour reading
alert(dataSet.hour[1]);
}
function getData(){
//i= drop-down selection index, set to zero for testing
var i=0;
var hour,db,wb;
var cities = ["AB Edmonton","MI Detroit"];
switch(cities[i]){
case "AB Edmonton":
hour = [1,2,3];
db = [10,11,12];
wb = [13,14,15];
break;
//case "MI Detroit":...
} //end of switch
return {hour: hour, db: db, wb: wb};
}; //end of getData

Related

Dates are not being passed in from Google Scripts to JavaScript

I have a problem where my Google Script function is not passing in date values to my JavaScript. Text and numerical values however are being passed in. The Google Scripts function searches my google sheets document to find a row of values based on a value that is passed into it. It then takes the data, puts it into an array and ships it over to my JavaScript function. The JavaScript then assigns the values from the array to my HTML document.
Here is my JavaScript:
function callDataRetriever(){
var number = document.getElementById("number").value;
google.script.run.withSuccessHandler(dataRetriever).retreiveData(number);
}
function dataRetriever(data){
document.getElementById("location").value = data[0]; //This works
document.getElementById("dateOpened").value = data[1]; //This does not work. Stops the function from continuing its task.
document.getElementById("value1").value = data[2]; //Without the date everything here down works
document.getElementById("value2").value = data[3];
document.getElementById("value2").value = data[4];
document.getElementById("value4").value = data[5];
//...
}
Here is my Google Scripts:
function retreiveData(number){
var url = "urlToSpreadsheet";
var ss = SpreadsheetApp.openByUrl(url);
var ws = ss.getSheetByName("Data");
var data = ws.getRange(1,1, ws.getLastRow(), ws.getLastColumn()).getValues();
var dataValues = [];
var filterData = data.filter(
function(r){
if(r[2] == number){
var i = 3;
while(i < 29){
dataValues.push(r[i]);
i++;
}
}
}
)
return dataValues;
}
In my Logs this is how it looks:
It is grabbing the date correctly however once passed into my JavaScript the function ceases to continue.
UPDATE:
Edited code based on doubleunary's suggestion. Now getting an error that I do not fully understand:
You cannot pass a Date object but will have to serialize it before sending. From the documentation:
Legal parameters and return values are JavaScript primitives like a Number, Boolean, String, or null, as well as JavaScript objects and arrays that are composed of primitives, objects and arrays. A form element within the page is also legal as a parameter, but it must be the function’s only parameter, and it is not legal as a return value. Requests fail if you attempt to pass a Date, Function, DOM element besides a form, or other prohibited type, including prohibited types inside objects or arrays. Objects that create circular references will also fail, and undefined fields within arrays become null.
Try using Utilities.formatDate(myDate, SpreadsheetApp.getActive().getSpreadsheetTimeZone(), 'yyyy-MM-dd') in the server side and new Date(data[1]) in the client side.
It seemed all I needed to do was add document.getElementById("dateValue").valueAsDate = new Date(data[1]); in my JavaScript and leave my Google-Script alone.

Writing a single element of an array to another sheet

I am trying to all the data from a range in a spreadsheet and filter it based on a certain condition where item[27] and item [29] equals "CO". It's working fine and filtering the range properly however I am trying to output only one item in the array which is item[3] and it's not doing that. It's giving me the filtered version of the entire range which is good but I don't want that.
This is basically a spreadsheet containing students and information about the work modules they have completed. I want to filter only the names of the students which is item[3] who have a "CO" or "complete" marked against a certain work module into another sheet. I tried to map the filtered data in another array using data.map and then tried to output just one element of the array and that is not working as well.
BTW this is my first time coding anything and using Google Apps script. I am really interested in this stuff and hopefully, one day be half as good as anyone here. Any help will me much appreciated.
function ReviewReport() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var mastersheet = ss.getSheetByName("Master Sheet 18-19-20");
var newtab = ss.getSheetByName("ReviewReportTest(Amith)");
var totalcolumns = mastersheet.getLastColumn();
var totalrows = mastersheet.getLastRow();
var masterdata = mastersheet
.getRange(4, 1, totalrows - 3, totalcolumns)
.getDisplayValues();
var data = masterdata.filter(function (item) {
// return item[27] === "CO" && item [29] === "CO";
if (item[27] === "CO" && item[29] === "CO") {
return item[3];
}
});
//tried to map the filtered data into another array below but this is not working as well.
var bsb = data.map(row);
function row(item) {
return item[3];
}
newtab.clearContents();
newtab.getRange(1, 1, data.length, data[0].length).setValues(data);
}
To write a single value from an Array object use Class Range setValue(value) where value could be data[rowIndex][columnIdx].
bsb was declared but not used later.
The code is adding the whole filtered data to the spreadsheet
The Array.prototype.map and it's callback are declared inside the same scope but map is used before the callback declaration. I think that it's better to declare it before calling it, or declare the callback global scope, but this is completely up to you. Think about style, readability and maintainability.
Resource
https://developers.google.com/apps-script/guides/sheets
Related
Declaring variables and functions, in what order?
Create a custom callback in JavaScript

indexOf running within Map function relying on Variable (array) from parent function (Google Apps Script)

I'm new to Google Apps Script/Javascript and I'm in the midst of working on the second iteration of my original code in order to make it faster. My first code made a lot of gets and sets to the underlying Google Sheet, while now I'm trying to do everything in arrays before setting the revised values. The current block of code I'm stuck on is my attempt to run an indexOf while running a map function.
The below script is about to run through every row of an array full of timesheet data and make
various amendments to the columns and calculate new column values.
var = amendedTimesheetData = timesheetdata.map(function(item){
item.splice(2, 0, item[0] + " " + item[1]);
item.splice(4, 0, item[0].slice(0, 2) + item[1].slice(0, 2) + date(item[3]));
item.splice(0, 2);
item.splice(3, 0, "Open");
item.splice(4, 0, accountingCode); //SEE ISSUE BELOW
return item
})
}
The accounting code variable relies on having performed a Javascript version of a vlookup based on a value in one of the array columns. The looked-up value exists in another array pulled from a different tab in the sheet. I know the code to perform the lookup looks like this:
var timeTrackingCode = item[6]; //this is the location in the mapped array of the value to be looked up
var timeTrackingCodeRow = codeMappingData.map(function(r){ return r[0]; }).indexOf(timeTrackingCode);
var accountingCode = codeMappingData[timeTrackingCodeRow][1]
The formula in the code above relies on the codeMappingData variable which was made earlier in the parent function before the map function started running.
var codeMappingSheet = ss.getSheetByName('CodeMapping');
var codeMappingRange = codeMappingSheet.getDataRange();
var codeMappingData = codeMappingRange.getValues();
The Question: How do I reference the codeMappingData array while in the map function?
Other options to get around this would be to run a For loop separate from the map, but while trying to learn all the possibilities I'm stuck on trying to understand how I could pass the variable through and achieve all of the column manipulations in as little code as possible. Any guidance on how I could achieve the variable pass through, or tips on how the code could be more efficient would all be very valuable. I've only copied a bit of the code, so it might lack context, but I think the question is clear.
Note that I've assumed that it would be ill-advised to establish the codeMappingData variable within the Map function because then every time it iterates through a row it would be performing a get from the sheet? If I'm incorrect in assuming this, then perhaps the simplest approach is to establish the variable within the map.
Thank you,
The inner or child scope always has access to variables in the outer or parent scope.
function outerFunction(){
const outerScopeVar = 5;
const arr = [1,2,3];
arr.map(num => {
console.info(`I have access to both inner and outer scope variable: ${num} and ${outerScopeVar}`);
})
}
outerFunction();
References:
Scope
You can use the thisArg parameter as described in the Array.prototype.map() documentation. Just add it after your closing curly bracket and refer to it as this within your map function, not codeMappingData.
var timeTrackingCodeRow = codeMappingData.map(function(r) {
Logger.log(this[0]); // logs the first element of codeMappingData
return r[0];
}, codeMappingData).indexOf(timeTrackingCode);

How do I prevent my program from overwriting localStorage every time a button is clicked?

document.getElementById("submit").addEventListener("click", getElements)
function getElements() {
var a = document.getElementById("sample").value;
var x = new obj(a);
function store() {
localStorage.setItem('todays-values', Object.values(x));
}
store();
}
In a separate js file I then call
localStorage.getItem('todays-values');
I get the values, but if I put new inputs into my html file and click the submit button, the previous values get overwritten and replaced by the new ones. How do I store all the values that are submitted and prevent the old ones from getting replaced?
I'm very new to Javascript so I would prefer to solve this problem without the use of any additional libraries if possible.
First: it seems that you are mixing JavaScript a class with a function (here is an example: What techniques can be used to define a class in JavaScript, and what are their trade-offs?)
For example this is the class equivalent in JavaScript:
function ClassName() {
var privateVar;
this.publicVar;
function privateFunction() {}
this.publicFunction = function() {};
}
You shouldn't wrap a function in a function unless it has a meaning (beacuse it is confusing for other people otherwise), but in the example given you don't need that. Also I can't see the reason why you are creating a new object x - if you create the object right before you save it you could just save the value because the object will only contain the value from sample, so you could write something like this:
document.getElementById("submit").addEventListener("click", getElements);
function storeElements() {
var sampleValue = document.getElementById("sample").value;
localStorage.setItem('todays-values', sampleValue);
}
Back to your question:
As Kalamarico mentioned: if you write new values into todays-values you will overwrite your old values, you could simply load all old values from the localStorage append the new ones and write them back to the localStorage.
You should also note that the localStorage only takes strings, so you should stringify objects (see localStorage.setItem).
function appendValueToStorage(key, value) {
var values = JSON.parse(localStorage.getItem(key));
if (values === null) {
values = [];
}
values.push(value);
localStorage.setItem(key, JSON.stringify(values));
console.log(localStorage.getItem(key));
}
appendValueToStorage('todays-values', document.getElementById("sample").value);
The function will let you append some value for a key, you could even wrap this function again to be able to use it in your click function:
function onSubmitClick() {
appendValueToStorage('todays-values', document.getElementById("sample").value);
}
document.getElementById("submit").addEventListener("click", onSubmitClick);
With the console.log command you can see the current content of the localStorage (you could also check with the developer tools - I find the ones for chrome work the best, under the Application -> Local Storage tab you can check the localStorage of your page).
You need read more about localStorage, this is a new feature introduced with HTML5, you can take a look here and see all features.
localStorage stores your data like a JSON object, if you don't know what is JSON, you need to find info. In javascript think in objects in this way:
var myData = {
myName: 'Kalamarico',
myAge: undefined
};
This is a Javascript object, and JSON is very similar and it is a representation of objects.
localStorage API stores your data as this way, when you do:
localStorage.setItem('todays-values', Object.values(x))
localStorage saves a new entry, one key 'todays-values' and its value is an object, so, your localStorage seems:
{
"todays-values": { ... }
}
Every time you set a "todays-values" you will overwrite the key, as you are seeing, so, if you can keep old values, you need to do this manage, first you can get items in localstorage (if there are), and after you can "merge" your old value and the new value. Or you can set a new key, for example: "todays-values1" depends on your need.
If you need to store exactly one key-value pair per day, then you could add the date in the key string.
Else how about numbering the keys ("yourKey_0", "yourKey_1", ...) and also storing the current (biggest) index ("currentIndex")in local storage:
function store(value) {
newIndex = localStorage.getItem("currentIndex") + 1;
localStorage.setItem("yourKey_" + newIndex, value);
localStorage.setItem("currentIndex", newIndex);
}
If you run into problems storing integer values, convert to strings.

Saving an array globally

What I'm trying to achieve
I have a spreadsheet with 2 sheets A & B.
A has 2 columns - Name, Amount (Master List)
B has 4 columns - Name, Amount, X, Y (Transaction List)
Name column of Sheet B references Name column of Sheet A for data. Whenever a name is selected, I want to populate Amount column in B with Amount in column of sheet A as a placeholder which users can override. For this, I plan to load the Sheet A data in an array (available Globally) so that in onEdit(e) I can refer that array instead of accessing Sheet B.
But the options I could find - CacheService and PropertyService - save only string values. But I want to have:
var myGlobalArray = [];
function on init(){
//iterate and fill the array such that it has following output
//myGlobalArray[Name1] = 1
//myGlobalArray[Name2] = 2
//myGlobalArray[Name3] = 3
}
function onEdit(e){
//if selected value is name1, populate myGolbalArray[Name1] value in Amount
}
Question
Where & how to define myGlobalArray?
I tried to use cache service with JSON.Stringify and JSON.parse but the array is empty in onEdit.
Each call to your script creates a new instance of your script with its own unique globals. Every time you call a script you will actually find a global "this" for that specific instance. You are correct to look at PropertyService as a persistent way to save data.
Right off I See that your globalArray is not set up right:
var myGlobalArray = [];
needs to be
var myGlobalArray = {};
myGlobalArray['name1'] = 1
myGlobalArray['name2'] = 2
myGlobalArray['name3'] = 3
//myGlobalArray = {name3=3.0, name1=1.0, name2=2.0}
var stringArray = JSON.stringify(myGlobalArray)
//{"name1":1,"name2":2,"name3":3};
Now that can be saved to and read from the property store.
PropertiesService.getScriptProperties().setProperty("NameArray", stringArray);
stringArray = PropertiesService.getScriptProperties().getProperty("NameArray");
myGlobalArray = JSON.parse(stringArray);
Logger.log(myGlobalArray['name1']) // returns 1
It's true that CacheService and PropertyService save only string values, but you can store any scalar data by using the JSON utilities JSON.stringify() and JSON.parse().
// Save an array in cache service
CacheService.getPublicCache()
.put("myGlobalArray", JSON.stringify(myGlobalArray));
// Retrieve an array from property service
var myGlobalArray = JSON.parse( CacheService.getPublicCache()
.get("myGlobalArray") );
// Save an array in property service
PropertiesService.getDocumentProperties()
.setProperty("myGlobalArray", JSON.stringify(myGlobalArray));
// Retrieve an array from property service
var myGlobalArray = JSON.parse( PropertiesService.getDocumentProperties()
.getProperty("myGlobalArray") );
When a variable is called "Global", we are referring to its scope, saying that it is available to all code within the same module. (You can read more about scope in What is the scope of variables in JavaScript?)
But since you're looking at CacheService and PropertyService, you already know that scope is only part of the problem. Each time that onEdit() is invoked, it will be running in a new execution instance on one of Google's servers. A value that had been in a global variable in a previous instance will not be available to this new instance. Therefore, we need to populate our "global variable" in each new invocation of our script.
An elegant way to reference global variables is as names properties of the special this object. For example, every function in our script can refer to this.myGlobalArray.1
You can adapt the getRssFeed() example from the Class Cache documentation into get_myGlobalArray(), say. Then your onEdit() trigger needs only to call that first to make sure that this.myGlobalArray contains the relevant array data.
function onEdit(e){
get_myGlobalArray();
//if selected value is name1, populate myGlobalArray[Name1] value in Amount
...
sheet.getRange(e.range.getRow(),2).setValue(myGlobalArray[e.value]);
}
/**
* Ensure the global variable "myGlobalArray" is defined and contains the
* values of column A in SheetA as an array.
*/
function get_myGlobalArray() {
if (typeof this.myGlobalArray == 'undefined') {
// Global variable doesn't exist, so need to populate it
// First, check for cached value
var cache = CacheService.getPublicCache();
var cached = cache.get("myGlobalArray");
if (cached) {
// We have a cached value, so parse it and store in global
this.myGlobalArray = JSON.parse(cached);
}
else {
// No value in the cache, so load it from spreadsheet
var data = SpreadsheetApp.getActive().getSheetByName("Sheet A").getDataRange().getValues();
this.myGlobalArray = {};
for (var row=0; row<data.length; row++) {
this.myGlobalArray[data[row][0]] = data[row][6];
}
// Stringify and store the global into the cache
cache.put("myGlobalArray", JSON.stringify(this.myGlobalArray));
}
}
}
Edit: Associative Array
In the comment within onEdit(), it's indicated:
//if selected value is name1, populate myGolbalArray[Name1] value in Amount
This implies that myGlobalArray is an associative array, where the index is a non-integer value. This requirement is now reflected in the way that this.myGlobalArray gets populated when read from the spreadsheet.
for (var row=0; row<data.length; row++) {
this.myGlobalArray[data[row][0]] = data[row][6];
// ^^^^^^^^^^^^ ^^^^^^^^^^^^
// Name ---------------/ /
// Amount ------------------------/
}
Much has been written about the different flavours of Javascript arrays, for instance Javascript Associative Arrays Demystified.
1 Actually, only functions with global scope would understand this to mean "global to the script". Functions that are contained inside objects would interpret this to mean their host object only. But that's a story for another day.

Categories

Resources